@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 +49 -21
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +19 -48
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +19 -48
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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.
|
|
50
|
-
| `country` | yes | ISO 3166-1 alpha-2 market, for example `US`.
|
|
51
|
-
| `language` | yes | ISO 639-1 language, for example `en`.
|
|
52
|
-
| `
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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.
|
|
27
|
-
*
|
|
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.
|
|
27
|
-
*
|
|
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(
|
|
1058
|
+
didYouMean: normalizeDidYouMean(
|
|
1059
|
+
response.didYouMean ?? response.did_you_mean
|
|
1060
|
+
)
|
|
1075
1061
|
};
|
|
1076
1062
|
}
|
|
1077
1063
|
return {
|
|
1078
|
-
suggestions:
|
|
1079
|
-
didYouMean: normalizeDidYouMean(
|
|
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
|
);
|