@nuskin/react-search-sdk 1.1.0-pa-1255.2 → 1.1.0

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/README.md CHANGED
@@ -1,11 +1,12 @@
1
1
  # @nuskin/react-search-sdk
2
2
 
3
3
  The search experience for the Nu Skin storefront: typeahead and recent searches over the Equinox
4
- catalog, and the NUi agent when a signed-in shopper searches for something with no products.
4
+ catalog, and the NUi agent when a shopper it is enabled for searches for something with no products.
5
5
 
6
6
  A host renders one component, `CommerceSearch`, and gives it the market, who the customer is, how to
7
7
  navigate, and its logo. The package works out the rest itself: the market's catalog lookup, the
8
- minimum search length, which NUi app to use, the panel's text and the analytics events. The panel UI
8
+ minimum search length and number of suggestions, which NUi app to use and for whom, the panel's text
9
+ and the analytics events. The panel UI
9
10
  comes from `@nuskin/react-search-components` and the agent from
10
11
  `@nuskin/react-nui-chat-sdk`.
11
12
 
@@ -41,30 +42,57 @@ import { CommerceSearch } from '@nuskin/react-search-sdk';
41
42
  />;
42
43
  ```
43
44
 
44
- | Prop | Required | Notes |
45
- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
46
- | `isOpen` | yes | The host owns whether the panel is showing. |
47
- | `onClose` | yes | Called for Cancel, Escape, the backdrop, and after `navigate`. |
48
- | `navigate` | yes | Receives `{ type: 'internal', path }` (for the router, market prefix already removed) or `{ type: 'external', url }` (full page load). The panel closes afterwards. |
49
- | `customer` | yes | `{ isAuthenticated, accountType?, userId? }`. Read on every request, so a change after sign-in is picked up. The agent is for signed-in visitors only. |
50
- | `country` | yes | ISO 3166-1 alpha-2 market, for example `US`. |
51
- | `language` | yes | ISO 639-1 language, for example `en`. |
52
- | `logo` | no | An element shown at the top of the panel. |
53
-
54
- The NUi agent also needs the `enableNuiSearch` flag, while that is the on/off switch: either
55
- `?enableNuiSearch=true` in the URL of the page the panel is opened on, or a cookie `enableNuiSearch=true`
56
- that you set by hand. The library only reads them and never copies the parameter into the cookie, so the
57
- parameter applies to that page alone. If both are present the parameter wins, so `?enableNuiSearch=false`
58
- switches the agent off for a page. It is for signed-in visitors only, unless the configuration sets
59
- `allowGuestAgent` (today only the test environment does, so the handoff can be tried where signing in
60
- is not possible).
45
+ | Prop | Required | Notes |
46
+ | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
47
+ | `isOpen` | yes | The host owns whether the panel is showing. |
48
+ | `onClose` | yes | Called for Cancel, Escape, the backdrop, and after `navigate`. |
49
+ | `navigate` | yes | Receives `{ type: 'internal', path }` (for the router, market prefix already removed) or `{ type: 'external', url }` (full page load). The panel closes afterwards. |
50
+ | `customer` | yes | `{ isAuthenticated, accountType?, userId? }`. Read on every request, so a change after sign-in is picked up. `accountType` decides which agent switch applies (see below). |
51
+ | `country` | yes | ISO 3166-1 alpha-2 market, for example `US`. |
52
+ | `language` | yes | ISO 639-1 language, for example `en`. |
53
+ | `clientId` | no | The client id the configuration service knows the host by. Without one the Configuration SDK uses the one in the page's run config; where there is none, the configuration and the suggestions cannot be read and the search does not work. |
54
+ | `logo` | no | An element shown at the top of the panel. |
55
+
56
+ ## Configuration
57
+
58
+ The package reads the `Commerce_Search` configuration map for the market, through
59
+ `@nuskin/configuration-sdk`, the first time the panel opens (and again at the next open if the read
60
+ failed). Until it has loaded, and whenever it cannot be read, the search is plain search with the
61
+ defaults below.
62
+
63
+ | Field | Default | Meaning |
64
+ | --------------------------------------------- | ------- | ------------------------------------------------------------------------------------------- |
65
+ | `commerceSearchMinimumLength` | `2` | Characters a term needs before the typeahead looks it up. A whole number, at least 1. |
66
+ | `commerceSearchMaxSuggestions` | `6` | Suggestions asked for and shown. A whole number, at least 1, and never more than 20. |
67
+ | `commerceSearchAgentAppKey` | none | The NUi app key. The agent needs this and the base URL; with only one of them it stays off. |
68
+ | `commerceSearchAgentApiBaseUrl` | none | The origin of the NUi API. |
69
+ | `commerceSearchAgentEnabledGuest` | `false` | The agent is on for visitors who are not signed in. |
70
+ | `commerceSearchAgentEnabledBrandAffiliate` | `false` | The agent is on for `Brand Affiliate` customers. |
71
+ | `commerceSearchAgentEnabledPreferredCustomer` | `false` | The agent is on for `Preferred Customer/Member` customers. |
72
+ | `commerceSearchAgentEnabledRetailCustomer` | `false` | The agent is on for `Retail Customer` customers. |
73
+
74
+ Anything missing or malformed falls back to its default, and a switch is on only when it is exactly
75
+ `true`. A visitor who is not signed in, or whose `accountType` is not one of the three above, is
76
+ treated as a guest.
77
+
78
+ ### The `enableNuiSearch` testing override
79
+
80
+ The `enableNuiSearch` flag overrides the configuration for the visitor who sets it, in either
81
+ direction: `true` switches the agent on and `false` switches it off, whatever the configuration says
82
+ for their kind of customer, and whether or not they are signed in. It is meant for testing, such as
83
+ trying the agent in production before it is enabled for anyone. An agent still has to be configured
84
+ (app key and base URL). Without the flag the configuration decides.
85
+
86
+ Set it with `?enableNuiSearch=true` in the URL of the page the panel is opened on, or with a cookie
87
+ `enableNuiSearch=true` that you set by hand. The library only reads them and never copies the
88
+ parameter into the cookie, so the parameter applies to that page alone. If both are present the
89
+ parameter wins, so `?enableNuiSearch=false` switches the agent off for a page. Anyone who knows the
90
+ flag can use it, so it is a testing switch and not an access control.
61
91
 
62
92
  ### Known gaps
63
93
 
64
94
  - The panel text is English only. It will be read from Contentstack once its content model exists.
65
95
  - Analytics events send `page_type` as `NA` until the data team defines its values.
66
- - The NUi settings and the Equinox client id are fixed values for the test environment in this
67
- package, to be replaced by a configuration store.
68
96
  - The `accountType` values are a best guess and need confirming against the suggestions API.
69
97
 
70
98
  ## Scripts
package/dist/index.d.mts CHANGED
@@ -23,8 +23,8 @@ type CustomerAccountType = 'guest' | 'Brand Affiliate' | 'Retail Customer' | 'Pr
23
23
  /** Who is searching. */
24
24
  interface CommerceSearchCustomer {
25
25
  /**
26
- * Whether the visitor is signed in. The NUi agent is for signed-in visitors only, unless the
27
- * package's configuration allows guests.
26
+ * Whether the visitor is signed in. A visitor who is not signed in is a guest: the
27
+ * configuration's guest switch decides whether the NUi agent is on for them.
28
28
  */
29
29
  isAuthenticated: boolean;
30
30
  /** Defaults to `guest`. Ignored for a visitor who is not signed in. */
package/dist/index.d.ts CHANGED
@@ -23,8 +23,8 @@ type CustomerAccountType = 'guest' | 'Brand Affiliate' | 'Retail Customer' | 'Pr
23
23
  /** Who is searching. */
24
24
  interface CommerceSearchCustomer {
25
25
  /**
26
- * Whether the visitor is signed in. The NUi agent is for signed-in visitors only, unless the
27
- * package's configuration allows guests.
26
+ * Whether the visitor is signed in. A visitor who is not signed in is a guest: the
27
+ * configuration's guest switch decides whether the NUi agent is on for them.
28
28
  */
29
29
  isAuthenticated: boolean;
30
30
  /** Defaults to `guest`. Ignored for a visitor who is not signed in. */
package/dist/index.js CHANGED
@@ -111,6 +111,7 @@ async function fetchCommerceSearchConfig({
111
111
  }
112
112
 
113
113
  // src/hooks/useCommerceSearchConfig.ts
114
+ var MISSING_CLIENT_ID_HINT = "[search] The configuration could not be read and no clientId was passed to CommerceSearch, so the Configuration SDK used the run config of the page. If the page has none, pass the clientId prop: without a client id the search cannot read its configuration or its suggestions.";
114
115
  function useCommerceSearchConfig({
115
116
  country,
116
117
  language,
@@ -124,6 +125,7 @@ function useCommerceSearchConfig({
124
125
  currentMarket.current = market;
125
126
  }, [market]);
126
127
  const reading = react.useRef(null);
128
+ const hasHintedClientId = react.useRef(false);
127
129
  react.useEffect(() => {
128
130
  if (!isOpen || loaded?.market === market || reading.current === market) {
129
131
  return;
@@ -142,6 +144,10 @@ function useCommerceSearchConfig({
142
144
  "[search] Could not load the product search configuration:",
143
145
  error
144
146
  );
147
+ if (!clientId && !hasHintedClientId.current) {
148
+ hasHintedClientId.current = true;
149
+ console.error(MISSING_CLIENT_ID_HINT);
150
+ }
145
151
  }).finally(() => {
146
152
  if (reading.current === market) reading.current = null;
147
153
  });
@@ -558,6 +564,7 @@ function createSearchService(options) {
558
564
  return catalogRequestConfigPromise;
559
565
  }
560
566
  const service = {
567
+ maxSuggestions,
561
568
  async fetchSuggestions(searchTerm) {
562
569
  const trimmedSearchTerm = String(searchTerm ?? "").trim();
563
570
  if (!trimmedSearchTerm || trimmedSearchTerm.length < minimumSearchLength) {
@@ -636,7 +643,8 @@ var fetchMarketConfig = async ({
636
643
  // src/utils/gtmConstants.ts
637
644
  var SEARCH_GTM_EVENTS = {
638
645
  CHAT_ENTRY_POINT: "Begin_chat",
639
- PRODUCT_SEARCH: "ProductSearch"};
646
+ PRODUCT_SEARCH: "ProductSearch"
647
+ };
640
648
  var SEARCH_GTM_SEARCH_TYPES = {
641
649
  SEMANTIC: "semantic search"};
642
650
  var SEARCH_GTM_USER_STATUS = {
@@ -793,17 +801,6 @@ function useRecentSearches({
793
801
  }
794
802
 
795
803
  // src/utils/catalogStatus.ts
796
- var CART_DISABLED_PRODUCT_STATUSES = /* @__PURE__ */ new Set([
797
- "discontinued",
798
- "preview",
799
- "previewproduct",
800
- "test",
801
- "stopped",
802
- "replacement",
803
- "bundleonly",
804
- "inactive",
805
- "notavailable"
806
- ]);
807
804
  function normalizeProductStatusValue(value) {
808
805
  return String(value ?? "").trim().toLowerCase().replace(/\s+/g, "");
809
806
  }
@@ -816,9 +813,6 @@ function resolveSkuStatusValues(suggestionOption) {
816
813
  ].map(normalizeProductStatusValue).filter(Boolean);
817
814
  }
818
815
  function isNonSellableStatusValue(statusValue) {
819
- if (CART_DISABLED_PRODUCT_STATUSES.has(statusValue)) {
820
- return true;
821
- }
822
816
  return statusValue !== "sellable" && statusValue !== "active";
823
817
  }
824
818
  function isCartActionDisabled(suggestionOption) {
@@ -1056,38 +1050,21 @@ function getVisibleSuggestions(opts) {
1056
1050
 
1057
1051
  // src/hooks/useSearchSuggestions.ts
1058
1052
  function normalizeSuggestionsResponse(response) {
1059
- if (Array.isArray(response)) {
1053
+ if (Array.isArray(response.product)) {
1060
1054
  return {
1061
- suggestions: response,
1062
- didYouMean: []
1063
- };
1064
- }
1065
- if (!response || typeof response !== "object") {
1066
- return { suggestions: [], didYouMean: [] };
1067
- }
1068
- const r = response;
1069
- if (Array.isArray(r.product)) {
1070
- return {
1071
- suggestions: r.product.map(
1055
+ suggestions: response.product.map(
1072
1056
  (product, index) => mapCatalogProductToSuggestion(product, index)
1073
1057
  ).filter((item) => item !== null),
1074
- didYouMean: normalizeDidYouMean(r.didYouMean ?? r.did_you_mean)
1058
+ didYouMean: normalizeDidYouMean(
1059
+ response.didYouMean ?? response.did_you_mean
1060
+ )
1075
1061
  };
1076
1062
  }
1077
1063
  return {
1078
- suggestions: Array.isArray(r.suggestions) ? r.suggestions : [],
1079
- didYouMean: normalizeDidYouMean(r.didYouMean)
1064
+ suggestions: response.suggestions,
1065
+ didYouMean: normalizeDidYouMean(response.didYouMean)
1080
1066
  };
1081
1067
  }
1082
- function resolveTotalResultsCount2(response, fallback) {
1083
- if (typeof response.totalResultsCount === "number") {
1084
- return response.totalResultsCount;
1085
- }
1086
- if (typeof response.pageableInfo?.totalCount === "number") {
1087
- return response.pageableInfo.totalCount;
1088
- }
1089
- return fallback;
1090
- }
1091
1068
  var FETCH_FAILED = {
1092
1069
  suggestions: [],
1093
1070
  didYouMean: [],
@@ -1106,10 +1083,7 @@ function toSettledResults(response, maxSuggestions) {
1106
1083
  failed: false
1107
1084
  };
1108
1085
  }
1109
- const total = Math.max(
1110
- resolveTotalResultsCount2(response, allSuggestions.length),
1111
- allSuggestions.length
1112
- );
1086
+ const total = Math.max(response.totalResultsCount, allSuggestions.length);
1113
1087
  const suggestions = allSuggestions.slice(0, maxSuggestions);
1114
1088
  return {
1115
1089
  suggestions,
@@ -1121,9 +1095,9 @@ function toSettledResults(response, maxSuggestions) {
1121
1095
  }
1122
1096
  function useSearchSuggestions({
1123
1097
  service,
1124
- enabled = true,
1125
- maxSuggestions = DEFAULT_MAX_SUGGESTIONS
1098
+ enabled = true
1126
1099
  }) {
1100
+ const { maxSuggestions } = service;
1127
1101
  const [query, setQuery] = react.useState("");
1128
1102
  const [suggestions, setSuggestions] = react.useState([]);
1129
1103
  const [didYouMean, setDidYouMean] = react.useState([]);
@@ -1552,7 +1526,6 @@ function CommerceSearchView({
1552
1526
  agentProducts,
1553
1527
  logo,
1554
1528
  minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH,
1555
- maxSuggestions = DEFAULT_MAX_SUGGESTIONS,
1556
1529
  gtmContext
1557
1530
  }) {
1558
1531
  const [handoff, setHandoff] = react.useState(null);
@@ -1560,7 +1533,6 @@ function CommerceSearchView({
1560
1533
  const [pendingTerm, setPendingTerm] = react.useState(null);
1561
1534
  const search = useSearchSuggestions({
1562
1535
  service,
1563
- maxSuggestions,
1564
1536
  enabled: isOpen && agentQuery === null
1565
1537
  });
1566
1538
  const {
@@ -1852,7 +1824,6 @@ function ValidMarketSearch({
1852
1824
  copy,
1853
1825
  logo,
1854
1826
  minimumSearchLength,
1855
- maxSuggestions,
1856
1827
  gtmContext
1857
1828
  }
1858
1829
  );