@nuskin/react-search-sdk 1.0.1-code-cleanup.2 → 1.1.0-pa-1255.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,4 +1,4 @@
1
- import { useState, useEffect, useRef, useMemo, useCallback, Component } from 'react';
1
+ import { useRef, useEffect, useCallback, useMemo, useState, Component } from 'react';
2
2
  import configurationSdk from '@nuskin/configuration-sdk';
3
3
  import { SearchPanel } from '@nuskin/react-search-components';
4
4
  import { jsx } from 'react/jsx-runtime';
@@ -16,36 +16,53 @@ var CATALOG_SUGGESTIONS_PATH = "/storefront/catalogs/search/suggestions";
16
16
  var RECENT_SEARCHES_PREFIX = "nuskin:header:recentSearches";
17
17
  var RECENT_SEARCHES_DEFAULT_LIMIT = 10;
18
18
  var DEFAULT_SUGGESTION_PAGE = 1;
19
- var DEFAULT_SUGGESTION_SIZE = 6;
20
19
  var DEFAULT_SEARCH_PATH = "/search";
21
20
  var DEBOUNCE_MS = 220;
22
21
  var MINIMUM_SEARCH_CHAR_LENGTH = 2;
23
- var MAX_VISIBLE_SUGGESTIONS = 6;
22
+ var DEFAULT_MAX_SUGGESTIONS = 6;
23
+ var MAX_SUGGESTIONS_CEILING = 20;
24
24
  var EQUINOX_MARKETS_MAP = "Equinox_Markets";
25
- var DEFAULT_EQUINOX_CLIENT_ID = "63056848d7532f9923f3d08cb48c423b";
25
+ var COMMERCE_SEARCH_MAP = "Commerce_Search";
26
26
 
27
27
  // src/config/commerceSearchConfig.ts
28
28
  var COMMERCE_SEARCH_CONFIG_DEFAULTS = Object.freeze({
29
- minimumSearchLength: MINIMUM_SEARCH_CHAR_LENGTH
29
+ minimumSearchLength: MINIMUM_SEARCH_CHAR_LENGTH,
30
+ maxSuggestions: DEFAULT_MAX_SUGGESTIONS,
31
+ agentEnabledFor: Object.freeze({
32
+ guest: false,
33
+ brandAffiliate: false,
34
+ preferredCustomer: false,
35
+ retailCustomer: false
36
+ })
30
37
  });
31
- var TEST_ENVIRONMENT_VALUES = {
32
- agent: {
33
- appKey: "pk_commerce_product_advisor_624aea4305b09240e8c8f41d0f6527b2",
34
- apiBaseUrl: "https://nui.tst.nuskin.ai/"
35
- },
36
- // Open to visitors who are not signed in, so the handoff can be tried where signing in is not
37
- // possible (for example local development). Sign-in stays required everywhere else.
38
- allowGuestAgent: true
38
+ var FIELDS = {
39
+ minimumLength: "commerceSearchMinimumLength",
40
+ maxSuggestions: "commerceSearchMaxSuggestions",
41
+ agentAppKey: "commerceSearchAgentAppKey",
42
+ agentApiBaseUrl: "commerceSearchAgentApiBaseUrl",
43
+ enabledGuest: "commerceSearchAgentEnabledGuest",
44
+ enabledBrandAffiliate: "commerceSearchAgentEnabledBrandAffiliate",
45
+ enabledPreferredCustomer: "commerceSearchAgentEnabledPreferredCustomer",
46
+ enabledRetailCustomer: "commerceSearchAgentEnabledRetailCustomer"
39
47
  };
40
48
  function nonEmptyString(value) {
41
49
  return typeof value === "string" ? value.trim() : "";
42
50
  }
51
+ function wholeNumber(value) {
52
+ const number = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
53
+ return typeof number === "number" && Number.isInteger(number) ? number : void 0;
54
+ }
55
+ function positiveWholeNumber(value) {
56
+ const number = wholeNumber(value);
57
+ return number !== void 0 && number >= 1 ? number : void 0;
58
+ }
43
59
  function normalizeCommerceSearchConfig(raw, { language } = {}) {
44
- const minimumSearchLength = Number(raw?.minimumSearchLength);
45
- const appKey = nonEmptyString(raw?.agent?.appKey);
46
- const apiBaseUrl = nonEmptyString(raw?.agent?.apiBaseUrl);
60
+ const appKey = nonEmptyString(raw?.[FIELDS.agentAppKey]);
61
+ const apiBaseUrl = nonEmptyString(raw?.[FIELDS.agentApiBaseUrl]);
62
+ const maxSuggestions = positiveWholeNumber(raw?.[FIELDS.maxSuggestions]);
47
63
  return {
48
- minimumSearchLength: Number.isInteger(minimumSearchLength) && minimumSearchLength >= 1 ? minimumSearchLength : COMMERCE_SEARCH_CONFIG_DEFAULTS.minimumSearchLength,
64
+ minimumSearchLength: positiveWholeNumber(raw?.[FIELDS.minimumLength]) ?? COMMERCE_SEARCH_CONFIG_DEFAULTS.minimumSearchLength,
65
+ maxSuggestions: maxSuggestions === void 0 ? COMMERCE_SEARCH_CONFIG_DEFAULTS.maxSuggestions : Math.min(maxSuggestions, MAX_SUGGESTIONS_CEILING),
49
66
  ...appKey && apiBaseUrl ? {
50
67
  agent: {
51
68
  appKey,
@@ -53,13 +70,36 @@ function normalizeCommerceSearchConfig(raw, { language } = {}) {
53
70
  ...language ? { language } : {}
54
71
  }
55
72
  } : {},
56
- ...raw?.allowGuestAgent === true ? { allowGuestAgent: true } : {}
73
+ agentEnabledFor: {
74
+ guest: raw?.[FIELDS.enabledGuest] === true,
75
+ brandAffiliate: raw?.[FIELDS.enabledBrandAffiliate] === true,
76
+ preferredCustomer: raw?.[FIELDS.enabledPreferredCustomer] === true,
77
+ retailCustomer: raw?.[FIELDS.enabledRetailCustomer] === true
78
+ }
57
79
  };
58
80
  }
59
81
  async function fetchCommerceSearchConfig({
60
- language
61
- } = {}) {
62
- const raw = getEnvironmentCode() === "test" ? TEST_ENVIRONMENT_VALUES : {};
82
+ country,
83
+ language,
84
+ clientId
85
+ }) {
86
+ const configuration = await getConfiguration([COMMERCE_SEARCH_MAP], {
87
+ country: country.trim().toUpperCase(),
88
+ ...clientId ? { clientId } : {}
89
+ });
90
+ const raw = configuration?.[COMMERCE_SEARCH_MAP];
91
+ if (!raw || Object.keys(raw).length === 0) {
92
+ throw new Error(
93
+ `the configuration service returned no ${COMMERCE_SEARCH_MAP} for market ${country}`
94
+ );
95
+ }
96
+ const hasKey = nonEmptyString(raw[FIELDS.agentAppKey]) !== "";
97
+ const hasUrl = nonEmptyString(raw[FIELDS.agentApiBaseUrl]) !== "";
98
+ if (hasKey !== hasUrl) {
99
+ console.warn(
100
+ `[search] The agent is half configured: ${FIELDS.agentAppKey} and ${FIELDS.agentApiBaseUrl} must both be set, or both empty. Using plain search.`
101
+ );
102
+ }
63
103
  return normalizeCommerceSearchConfig(raw, language ? { language } : {});
64
104
  }
65
105
 
@@ -67,30 +107,39 @@ async function fetchCommerceSearchConfig({
67
107
  function useCommerceSearchConfig({
68
108
  country,
69
109
  language,
70
- enabled
110
+ clientId,
111
+ isOpen
71
112
  }) {
72
- const [config, setConfig] = useState(
73
- COMMERCE_SEARCH_CONFIG_DEFAULTS
74
- );
113
+ const market = `${country}|${language}|${clientId ?? ""}`;
114
+ const [loaded, setLoaded] = useState(null);
115
+ const currentMarket = useRef(market);
75
116
  useEffect(() => {
76
- if (!enabled) return void 0;
77
- let isStale = false;
117
+ currentMarket.current = market;
118
+ }, [market]);
119
+ const reading = useRef(null);
120
+ useEffect(() => {
121
+ if (!isOpen || loaded?.market === market || reading.current === market) {
122
+ return;
123
+ }
124
+ reading.current = market;
78
125
  fetchCommerceSearchConfig({
79
- ...country ? { country } : {},
80
- ...language ? { language } : {}
81
- }).then((next) => {
82
- if (!isStale) setConfig(next);
126
+ country,
127
+ language,
128
+ ...clientId ? { clientId } : {}
129
+ }).then((config) => {
130
+ if (currentMarket.current === market) {
131
+ setLoaded({ market, config });
132
+ }
83
133
  }).catch((error) => {
84
134
  console.warn(
85
135
  "[search] Could not load the product search configuration:",
86
136
  error
87
137
  );
138
+ }).finally(() => {
139
+ if (reading.current === market) reading.current = null;
88
140
  });
89
- return () => {
90
- isStale = true;
91
- };
92
- }, [enabled, country, language]);
93
- return config;
141
+ }, [isOpen, market, loaded, country, language, clientId]);
142
+ return loaded?.market === market ? loaded.config : COMMERCE_SEARCH_CONFIG_DEFAULTS;
94
143
  }
95
144
 
96
145
  // src/copy/searchPanelCopy.ts
@@ -229,13 +278,14 @@ function buildCatalogSuggestionsUrl(apiBaseUrl) {
229
278
  function buildSuggestionsRequestUrl({
230
279
  suggestionsUrl,
231
280
  searchTerm,
281
+ size,
232
282
  storeId,
233
283
  localeParts
234
284
  }) {
235
285
  const queryParams = new URLSearchParams({
236
286
  searchTerm,
237
287
  page: String(DEFAULT_SUGGESTION_PAGE),
238
- size: String(DEFAULT_SUGGESTION_SIZE),
288
+ size: String(size),
239
289
  locale: toCatalogLocale(localeParts),
240
290
  storeId
241
291
  });
@@ -476,7 +526,8 @@ function createSearchService(options) {
476
526
  language,
477
527
  getMarketConfig,
478
528
  getCustomerType,
479
- minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH
529
+ minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH,
530
+ maxSuggestions = DEFAULT_MAX_SUGGESTIONS
480
531
  } = options;
481
532
  const locationLike = getBrowserLocation(providedLocation);
482
533
  const storage = getSessionStorage(providedStorage);
@@ -511,6 +562,7 @@ function createSearchService(options) {
511
562
  buildSuggestionsRequestUrl({
512
563
  suggestionsUrl,
513
564
  searchTerm: trimmedSearchTerm,
565
+ size: maxSuggestions,
514
566
  storeId,
515
567
  localeParts
516
568
  }),
@@ -560,12 +612,15 @@ function createSearchService(options) {
560
612
  }
561
613
 
562
614
  // src/services/marketConfig.ts
563
- var fetchMarketConfig = async ({ country }) => {
615
+ var fetchMarketConfig = async ({
616
+ country,
617
+ clientId
618
+ }) => {
564
619
  const countryCode = country.trim().toUpperCase();
565
620
  const configuration = await getConfiguration([EQUINOX_MARKETS_MAP], {
566
621
  country: countryCode,
567
622
  ...getEnvironmentCode() ? { environment: getEnvironmentCode() } : {},
568
- clientId: DEFAULT_EQUINOX_CLIENT_ID,
623
+ ...clientId ? { clientId } : {},
569
624
  shoppingContext: STOREFRONT_SHOPPING_CONTEXT
570
625
  });
571
626
  return configuration?.[EQUINOX_MARKETS_MAP] ?? null;
@@ -683,29 +738,23 @@ function resolveNuiSearchFlag(env = {}) {
683
738
  return parseFlagValue(new URLSearchParams(search).get(NUI_SEARCH_FLAG)) ?? readCookieFlag(cookie);
684
739
  }
685
740
  function isNuiSearchEnabled({
686
- isAuthenticated,
687
741
  flag,
688
- allowGuests = false
742
+ enabledForCustomer
689
743
  }) {
690
- return flag === true && (isAuthenticated === true || allowGuests);
744
+ return flag ?? enabledForCustomer;
691
745
  }
692
746
 
693
747
  // src/hooks/useNuiAgentGate.ts
694
748
  function useNuiAgentGate({
695
749
  hasAgent,
696
750
  isOpen,
697
- isAuthenticated,
698
- allowGuestAgent
751
+ enabledForCustomer
699
752
  }) {
700
753
  const [flag, setFlag] = useState(void 0);
701
754
  useEffect(() => {
702
755
  if (isOpen) setFlag(resolveNuiSearchFlag());
703
756
  }, [isOpen]);
704
- return hasAgent && isNuiSearchEnabled({
705
- isAuthenticated,
706
- flag,
707
- allowGuests: allowGuestAgent
708
- });
757
+ return hasAgent && isNuiSearchEnabled({ flag, enabledForCustomer });
709
758
  }
710
759
  function uniqueRecentSearches(queries) {
711
760
  const seen = /* @__PURE__ */ new Set();
@@ -1039,7 +1088,7 @@ var FETCH_FAILED = {
1039
1088
  emptyFetched: true,
1040
1089
  failed: true
1041
1090
  };
1042
- function toSettledResults(response) {
1091
+ function toSettledResults(response, maxSuggestions) {
1043
1092
  const { suggestions: allSuggestions, didYouMean } = normalizeSuggestionsResponse(response);
1044
1093
  if (didYouMean.length > 0) {
1045
1094
  return {
@@ -1054,18 +1103,19 @@ function toSettledResults(response) {
1054
1103
  resolveTotalResultsCount2(response, allSuggestions.length),
1055
1104
  allSuggestions.length
1056
1105
  );
1057
- const suggestions = allSuggestions.slice(0, MAX_VISIBLE_SUGGESTIONS);
1106
+ const suggestions = allSuggestions.slice(0, maxSuggestions);
1058
1107
  return {
1059
1108
  suggestions,
1060
1109
  didYouMean,
1061
- hasMoreResults: total > MAX_VISIBLE_SUGGESTIONS,
1110
+ hasMoreResults: total > maxSuggestions,
1062
1111
  emptyFetched: suggestions.length === 0,
1063
1112
  failed: false
1064
1113
  };
1065
1114
  }
1066
1115
  function useSearchSuggestions({
1067
1116
  service,
1068
- enabled = true
1117
+ enabled = true,
1118
+ maxSuggestions = DEFAULT_MAX_SUGGESTIONS
1069
1119
  }) {
1070
1120
  const [query, setQuery] = useState("");
1071
1121
  const [suggestions, setSuggestions] = useState([]);
@@ -1122,7 +1172,8 @@ function useSearchSuggestions({
1122
1172
  let results;
1123
1173
  try {
1124
1174
  results = toSettledResults(
1125
- await service.fetchSuggestions(trimmedQuery)
1175
+ await service.fetchSuggestions(trimmedQuery),
1176
+ maxSuggestions
1126
1177
  );
1127
1178
  } catch (error) {
1128
1179
  console.warn("[search] Could not load suggestions:", error);
@@ -1132,7 +1183,7 @@ function useSearchSuggestions({
1132
1183
  settle(results, trimmedQuery);
1133
1184
  setLoading(false);
1134
1185
  },
1135
- [enabled, service, resetSuggestionsState, settle]
1186
+ [enabled, maxSuggestions, service, resetSuggestionsState, settle]
1136
1187
  );
1137
1188
  useEffect(() => {
1138
1189
  latestRequestId.current += 1;
@@ -1489,12 +1540,12 @@ function CommerceSearchView({
1489
1540
  onClose,
1490
1541
  service,
1491
1542
  navigate,
1492
- isAuthenticated,
1493
1543
  agent,
1494
- allowGuestAgent = false,
1544
+ agentEnabledForCustomer,
1495
1545
  agentProducts,
1496
1546
  logo,
1497
1547
  minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH,
1548
+ maxSuggestions = DEFAULT_MAX_SUGGESTIONS,
1498
1549
  gtmContext
1499
1550
  }) {
1500
1551
  const [handoff, setHandoff] = useState(null);
@@ -1502,6 +1553,7 @@ function CommerceSearchView({
1502
1553
  const [pendingTerm, setPendingTerm] = useState(null);
1503
1554
  const search = useSearchSuggestions({
1504
1555
  service,
1556
+ maxSuggestions,
1505
1557
  enabled: isOpen && agentQuery === null
1506
1558
  });
1507
1559
  const {
@@ -1517,8 +1569,7 @@ function CommerceSearchView({
1517
1569
  const agentEnabled = useNuiAgentGate({
1518
1570
  hasAgent: agent !== void 0,
1519
1571
  isOpen,
1520
- isAuthenticated,
1521
- allowGuestAgent
1572
+ enabledForCustomer: agentEnabledForCustomer
1522
1573
  });
1523
1574
  const { recentSearches, clearRecentSearches } = useRecentSearches({
1524
1575
  service,
@@ -1676,6 +1727,15 @@ function CommerceSearchView({
1676
1727
  );
1677
1728
  }
1678
1729
  var GUEST = "guest";
1730
+ var AGENT_SWITCH_FOR = {
1731
+ guest: "guest",
1732
+ "Brand Affiliate": "brandAffiliate",
1733
+ "Retail Customer": "retailCustomer",
1734
+ "Preferred Customer/Member": "preferredCustomer"
1735
+ };
1736
+ var AGENT_SWITCHES = new Map(
1737
+ Object.entries(AGENT_SWITCH_FOR)
1738
+ );
1679
1739
  function CommerceSearch(props) {
1680
1740
  if (!isValidMarket(props.country, props.language)) {
1681
1741
  return /* @__PURE__ */ jsx(
@@ -1698,32 +1758,45 @@ function ValidMarketSearch({
1698
1758
  customer,
1699
1759
  country,
1700
1760
  language,
1761
+ clientId,
1701
1762
  logo
1702
1763
  }) {
1703
- const [hasOpened, setHasOpened] = useState(false);
1704
- useEffect(() => {
1705
- if (isOpen) setHasOpened(true);
1706
- }, [isOpen]);
1707
1764
  const copy = useSearchPanelCopy({ language });
1708
- const { minimumSearchLength, agent, allowGuestAgent } = useCommerceSearchConfig({
1765
+ const { minimumSearchLength, maxSuggestions, agent, agentEnabledFor } = useCommerceSearchConfig({
1709
1766
  country,
1710
1767
  language,
1711
- enabled: hasOpened
1768
+ ...clientId ? { clientId } : {},
1769
+ isOpen
1712
1770
  });
1713
1771
  const customerType = customer.isAuthenticated ? customer.accountType ?? GUEST : GUEST;
1714
1772
  const customerTypeRef = useRef(customerType);
1715
1773
  useEffect(() => {
1716
1774
  customerTypeRef.current = customerType;
1717
1775
  }, [customerType]);
1776
+ const agentEnabledForCustomer = agentEnabledFor[AGENT_SWITCHES.get(customerType) ?? "guest"];
1777
+ const getMarketConfig = useCallback(
1778
+ (context) => fetchMarketConfig({
1779
+ ...context,
1780
+ ...clientId ? { clientId } : {}
1781
+ }),
1782
+ [clientId]
1783
+ );
1718
1784
  const service = useMemo(
1719
1785
  () => createSearchService({
1720
1786
  country,
1721
1787
  language,
1722
- getMarketConfig: fetchMarketConfig,
1788
+ getMarketConfig,
1723
1789
  minimumSearchLength,
1790
+ maxSuggestions,
1724
1791
  getCustomerType: () => customerTypeRef.current
1725
1792
  }),
1726
- [country, language, minimumSearchLength]
1793
+ [
1794
+ country,
1795
+ language,
1796
+ getMarketConfig,
1797
+ minimumSearchLength,
1798
+ maxSuggestions
1799
+ ]
1727
1800
  );
1728
1801
  const navigateToPath = useCallback(
1729
1802
  (path) => {
@@ -1766,13 +1839,13 @@ function ValidMarketSearch({
1766
1839
  onClose,
1767
1840
  service,
1768
1841
  navigate: navigateToPath,
1769
- isAuthenticated,
1770
1842
  ...agent ? { agent } : {},
1771
- ...allowGuestAgent ? { allowGuestAgent } : {},
1843
+ agentEnabledForCustomer,
1772
1844
  ...agentProducts ? { agentProducts } : {},
1773
1845
  copy,
1774
1846
  logo,
1775
1847
  minimumSearchLength,
1848
+ maxSuggestions,
1776
1849
  gtmContext
1777
1850
  }
1778
1851
  );