@nuskin/react-search-sdk 1.0.0-trying-again.2 → 1.0.0-trying-again.4

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
@@ -7,14 +7,35 @@ A host renders one component, `CommerceSearch`, and gives it the market, who the
7
7
  navigate, and its logo. The package works out the rest itself: the market's catalog lookup, the
8
8
  minimum search length, which NUi app to use, the panel's text and the analytics events. The panel UI
9
9
  comes from [`@nuskin/react-search-components`](../react-search-components) and the agent from
10
- `@nuskin/react-nui-chat-sdk`.
10
+ `@nu-skin-enterprises/chat-widget`, the platform-owned chat widget published from
11
+ `sales-ai-platform` to AWS CodeArtifact (see [Installing](#installing)).
12
+
13
+ ## Installing
14
+
15
+ `@nu-skin-enterprises/chat-widget` is not on npmjs. `.npmrc` routes only that scope to the
16
+ `npm-sdks` repository of the `ai-spr-sdk` CodeArtifact domain (account 105371274045, us-west-2) and
17
+ reads a token from `CODEARTIFACT_AUTH_TOKEN`. CI mints it in `.gitlab-ci.yml`; locally, with an AWS
18
+ identity that may read the domain (the `nse-dev` and `nse-nnp` developer permission sets can):
19
+
20
+ ```sh
21
+ export CODEARTIFACT_AUTH_TOKEN="$(aws codeartifact get-authorization-token \
22
+ --domain ai-spr-sdk --domain-owner 105371274045 --region us-west-2 \
23
+ --query authorizationToken --output text)" # valid 12 hours
24
+ yarn install
25
+ ```
26
+
27
+ The domain owner matters: the dev account has an unrelated domain of the same name.
28
+
29
+ The variable must exist for yarn to start at all, even with the package already installed: yarn
30
+ refuses an `.npmrc` that references an unset variable. An empty value is enough for commands that
31
+ do not fetch (`export CODEARTIFACT_AUTH_TOKEN=`).
11
32
 
12
33
  ## Requirements
13
34
 
14
35
  - Node.js `>=22.0.0 <25.0.0`
15
36
  - Peer dependencies: `react` and `react-dom` `^18.2.0`, `@mui/material`, `@emotion/react`,
16
37
  `@emotion/styled`, `@nuskin/foundation-theme`, `@nuskin/foundation-ui-components` `4.x`, and
17
- `@nuskin/configuration-sdk` `>=3.1.0-0` (it reads the market configuration and tells the package
38
+ `@nuskin/configuration-sdk` `^3.1.0` (it reads the market configuration and tells the package
18
39
  which environment it is in).
19
40
 
20
41
  ## Usage
@@ -52,7 +73,9 @@ import { CommerceSearch } from '@nuskin/react-search-sdk';
52
73
  | `logo` | no | An element shown at the top of the panel. |
53
74
 
54
75
  The NUi agent also needs the `enableNuiSearch` cookie, or `?enableNuiSearch=true` in the URL, while
55
- that is the on/off switch.
76
+ that is the on/off switch. It is for signed-in visitors only, unless the configuration sets
77
+ `allowGuestAgent` (today only the test environment does, so the handoff can be tried where signing in
78
+ is not possible).
56
79
 
57
80
  ### Known gaps
58
81
 
package/dist/index.d.mts CHANGED
@@ -22,7 +22,10 @@ type NavigationTarget = {
22
22
  type CustomerAccountType = 'guest' | 'Brand Affiliate' | 'Retail Customer' | 'Preferred Customer/Member';
23
23
  /** Who is searching. */
24
24
  interface CommerceSearchCustomer {
25
- /** Whether the visitor is signed in. The NUi agent is for signed-in visitors only. */
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.
28
+ */
26
29
  isAuthenticated: boolean;
27
30
  /** Defaults to `guest`. Ignored for a visitor who is not signed in. */
28
31
  accountType?: CustomerAccountType;
@@ -49,7 +52,7 @@ interface CommerceSearchProps {
49
52
  /**
50
53
  * The commerce website's single search entry point: typeahead and recent searches over the
51
54
  * product catalog today, with the NUi agent taking over in the same panel when a signed-in
52
- * shopper searches for something with no products. What it can answer is not limited to
55
+ * shopper (guests too, where the configuration allows it) searches for something with no products. What it can answer is not limited to
53
56
  * products — it is the commerce site's search, not a product-only search box.
54
57
  *
55
58
  * The package works out the rest itself: the market's catalog lookup, the minimum search length,
package/dist/index.d.ts CHANGED
@@ -22,7 +22,10 @@ type NavigationTarget = {
22
22
  type CustomerAccountType = 'guest' | 'Brand Affiliate' | 'Retail Customer' | 'Preferred Customer/Member';
23
23
  /** Who is searching. */
24
24
  interface CommerceSearchCustomer {
25
- /** Whether the visitor is signed in. The NUi agent is for signed-in visitors only. */
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.
28
+ */
26
29
  isAuthenticated: boolean;
27
30
  /** Defaults to `guest`. Ignored for a visitor who is not signed in. */
28
31
  accountType?: CustomerAccountType;
@@ -49,7 +52,7 @@ interface CommerceSearchProps {
49
52
  /**
50
53
  * The commerce website's single search entry point: typeahead and recent searches over the
51
54
  * product catalog today, with the NUi agent taking over in the same panel when a signed-in
52
- * shopper searches for something with no products. What it can answer is not limited to
55
+ * shopper (guests too, where the configuration allows it) searches for something with no products. What it can answer is not limited to
53
56
  * products — it is the commerce site's search, not a product-only search box.
54
57
  *
55
58
  * The package works out the rest itself: the market's catalog lookup, the minimum search length,
package/dist/index.js CHANGED
@@ -3,11 +3,14 @@
3
3
  var react = require('react');
4
4
  var configurationSdk = require('@nuskin/configuration-sdk');
5
5
  var reactSearchComponents = require('@nuskin/react-search-components');
6
+ var reactProductSdk = require('@nuskin/react-product-sdk');
7
+ var styled = require('@emotion/styled');
6
8
  var jsxRuntime = require('react/jsx-runtime');
7
9
 
8
10
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
11
 
10
12
  var configurationSdk__default = /*#__PURE__*/_interopDefault(configurationSdk);
13
+ var styled__default = /*#__PURE__*/_interopDefault(styled);
11
14
 
12
15
  // src/components/CommerceSearch/CommerceSearch.tsx
13
16
  var { getConfiguration, getEnvironmentCode } = configurationSdk__default.default;
@@ -34,7 +37,10 @@ var TEST_ENVIRONMENT_VALUES = {
34
37
  agent: {
35
38
  appKey: "pk_commerce_product_advisor_624aea4305b09240e8c8f41d0f6527b2",
36
39
  apiBaseUrl: "https://nui.tst.nuskin.ai/"
37
- }
40
+ },
41
+ // Open to visitors who are not signed in, so the handoff can be tried where signing in is not
42
+ // possible (for example local development). Sign-in stays required everywhere else.
43
+ allowGuestAgent: true
38
44
  };
39
45
  function nonEmptyString(value) {
40
46
  return typeof value === "string" ? value.trim() : "";
@@ -51,7 +57,8 @@ function normalizeCommerceSearchConfig(raw, { language } = {}) {
51
57
  apiBaseUrl,
52
58
  ...language ? { language } : {}
53
59
  }
54
- } : {}
60
+ } : {},
61
+ ...raw?.allowGuestAgent === true ? { allowGuestAgent: true } : {}
55
62
  };
56
63
  }
57
64
  async function fetchCommerceSearchConfig({
@@ -305,6 +312,16 @@ function resolveCustomerTypeHeaders(getCustomerType) {
305
312
  const customerType = getCustomerType?.()?.trim();
306
313
  return customerType ? { "X-Customer-Type": customerType } : void 0;
307
314
  }
315
+ var NO_RESULTS_ERROR_CODE = "SFOSER002";
316
+ async function isNoResultsResponse(response) {
317
+ if (response.status !== 404) return false;
318
+ try {
319
+ const body = await response.json();
320
+ return body?.code === NO_RESULTS_ERROR_CODE;
321
+ } catch {
322
+ return false;
323
+ }
324
+ }
308
325
  async function fetchJsonFromCandidates(fetchImpl, requestUrls, requestHeaders = {}) {
309
326
  let lastError;
310
327
  for (const requestUrl of requestUrls) {
@@ -315,6 +332,7 @@ async function fetchJsonFromCandidates(fetchImpl, requestUrls, requestHeaders =
315
332
  headers: { Accept: "application/json", ...requestHeaders }
316
333
  });
317
334
  if (!response.ok) {
335
+ if (await isNoResultsResponse(response)) return null;
318
336
  lastError = new Error(
319
337
  `Request failed with status ${response.status}`
320
338
  );
@@ -354,6 +372,13 @@ function createSearchService(options = {}) {
354
372
  locationLike,
355
373
  country: localeParts.country,
356
374
  getMarketConfig
375
+ }).catch((error) => {
376
+ catalogRequestConfigPromise = null;
377
+ console.warn(
378
+ "[search] Could not load the market configuration for the suggestions:",
379
+ error
380
+ );
381
+ throw error;
357
382
  });
358
383
  }
359
384
  return catalogRequestConfigPromise;
@@ -474,6 +499,19 @@ function toNavigationTarget(path) {
474
499
  return /^https?:\/\//i.test(path) ? { type: "external", url: path } : { type: "internal", path };
475
500
  }
476
501
 
502
+ // src/utils/productEnvironment.ts
503
+ var PRODUCT_ENVIRONMENTS = ["dev", "test", "prod"];
504
+ function toProductEnvironment(environmentCode) {
505
+ return environmentCode && PRODUCT_ENVIRONMENTS.includes(environmentCode) ? environmentCode : void 0;
506
+ }
507
+ function getProductEnvironment() {
508
+ try {
509
+ return toProductEnvironment(getEnvironmentCode());
510
+ } catch {
511
+ return void 0;
512
+ }
513
+ }
514
+
477
515
  // src/utils/catalogHelpers.ts
478
516
  var CART_DISABLED_PRODUCT_STATUSES = /* @__PURE__ */ new Set([
479
517
  "discontinued",
@@ -1003,9 +1041,10 @@ function resolveNuiSearchFlag(env = {}) {
1003
1041
  }
1004
1042
  function isNuiSearchEnabled({
1005
1043
  isAuthenticated,
1006
- flag
1044
+ flag,
1045
+ allowGuests = false
1007
1046
  }) {
1008
- return isAuthenticated === true && flag === true;
1047
+ return flag === true && (isAuthenticated === true || allowGuests);
1009
1048
  }
1010
1049
 
1011
1050
  // src/utils/searchRouting.ts
@@ -1019,15 +1058,69 @@ function decideSearchRoute({
1019
1058
  if (snapshot.suggestionFetchFailed) return "search";
1020
1059
  return snapshot.suggestions.length > 0 ? "search" : "handoff";
1021
1060
  }
1061
+
1062
+ // src/utils/cachedProductClient.ts
1063
+ function withProductCache(client) {
1064
+ const lookups = /* @__PURE__ */ new Map();
1065
+ return {
1066
+ getProductById(productId) {
1067
+ let lookup = lookups.get(productId);
1068
+ if (!lookup) {
1069
+ lookup = client.getProductById(productId);
1070
+ lookups.set(productId, lookup);
1071
+ }
1072
+ return lookup;
1073
+ },
1074
+ getProductsByIds: (productIds) => client.getProductsByIds(productIds)
1075
+ };
1076
+ }
1077
+ var AgentProductSlot = styled__default.default.div`
1078
+ && {
1079
+ flex: 0 0 240px;
1080
+ overflow: hidden;
1081
+ width: 240px;
1082
+ }
1083
+ `;
1084
+ var SalesAIChatWidget = react.lazy(
1085
+ () => Promise.all([
1086
+ import('@nu-skin-enterprises/chat-widget'),
1087
+ import('@nu-skin-enterprises/chat-widget/styles.css')
1088
+ ]).then(([mod]) => ({ default: mod.SalesAIChatWidget }))
1089
+ );
1022
1090
  var HANDLE_POLL_MS = 50;
1023
1091
  var HANDLE_TIMEOUT_MS = 15e3;
1024
- var SalesAIChatWidget = react.lazy(async () => {
1025
- const [widget] = await Promise.all([
1026
- import('@nuskin/react-nui-chat-sdk'),
1027
- import('@nuskin/react-nui-chat-sdk/styles.css')
1028
- ]);
1029
- return { default: widget.SalesAIChatWidget };
1030
- });
1092
+ function AgentProductCard({ sku }) {
1093
+ const { product, isLoading, error } = reactProductSdk.useProductById(sku);
1094
+ if (!isLoading && (error || !product?.seoInformation?.canonicalURL)) {
1095
+ return null;
1096
+ }
1097
+ return /* @__PURE__ */ jsxRuntime.jsx(AgentProductSlot, { children: /* @__PURE__ */ jsxRuntime.jsx(reactProductSdk.ProductCard, { productId: sku }) });
1098
+ }
1099
+ var renderAgentProduct = (product) => /* @__PURE__ */ jsxRuntime.jsx(AgentProductCard, { sku: product.sku });
1100
+ function AgentWidget({
1101
+ widgetProps,
1102
+ products
1103
+ }) {
1104
+ const environment = products?.environment;
1105
+ const market = products?.market;
1106
+ const language = products?.language;
1107
+ const client = react.useMemo(
1108
+ () => environment && market && language ? withProductCache(
1109
+ reactProductSdk.createProductClient({ environment, market, language })
1110
+ ) : void 0,
1111
+ [environment, market, language]
1112
+ );
1113
+ if (!client) {
1114
+ return /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(SalesAIChatWidget, { ...widgetProps }) });
1115
+ }
1116
+ return /* @__PURE__ */ jsxRuntime.jsx(reactProductSdk.ProductProvider, { client, children: /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(
1117
+ SalesAIChatWidget,
1118
+ {
1119
+ renderProduct: renderAgentProduct,
1120
+ ...widgetProps
1121
+ }
1122
+ ) }) });
1123
+ }
1031
1124
  function getSalesAIHandle() {
1032
1125
  return window.SalesAI;
1033
1126
  }
@@ -1083,6 +1176,7 @@ function NuiAgentPanel({
1083
1176
  agent,
1084
1177
  query,
1085
1178
  handoffContext,
1179
+ products,
1086
1180
  onUnavailable
1087
1181
  }) {
1088
1182
  useHandoff({
@@ -1091,19 +1185,22 @@ function NuiAgentPanel({
1091
1185
  agentContext: agent.context,
1092
1186
  onUnavailable
1093
1187
  });
1094
- return /* @__PURE__ */ jsxRuntime.jsx(AgentErrorBoundary, { onError: onUnavailable, children: /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(
1095
- SalesAIChatWidget,
1188
+ const widgetProps = {
1189
+ appKey: agent.appKey,
1190
+ apiBaseUrl: agent.apiBaseUrl,
1191
+ theme: agent.theme ?? "nui-light",
1192
+ defaultOpen: true,
1193
+ hideFab: true,
1194
+ placement: "host",
1195
+ ...agent.language === void 0 ? {} : { language: agent.language }
1196
+ };
1197
+ return /* @__PURE__ */ jsxRuntime.jsx(AgentErrorBoundary, { onError: onUnavailable, children: /* @__PURE__ */ jsxRuntime.jsx(
1198
+ AgentWidget,
1096
1199
  {
1097
- appKey: agent.appKey,
1098
- apiBaseUrl: agent.apiBaseUrl,
1099
- theme: agent.theme ?? "nui-light",
1100
- defaultOpen: true,
1101
- hideFab: true,
1102
- placement: "host",
1103
- ...agent.language === void 0 ? {} : { language: agent.language },
1104
- ...agent.renderProduct === void 0 ? {} : { renderProduct: agent.renderProduct }
1200
+ widgetProps,
1201
+ ...products ? { products } : {}
1105
1202
  }
1106
- ) }) });
1203
+ ) });
1107
1204
  }
1108
1205
  var SUBMIT_WAIT_TIMEOUT_MS = 2e3;
1109
1206
  function uniqueRecentSearches(queries) {
@@ -1122,6 +1219,8 @@ function CommerceSearchView({
1122
1219
  navigate,
1123
1220
  isAuthenticated,
1124
1221
  agent,
1222
+ allowGuestAgent = false,
1223
+ agentProducts,
1125
1224
  logo,
1126
1225
  minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH,
1127
1226
  gtmContext
@@ -1147,7 +1246,11 @@ function CommerceSearchView({
1147
1246
  suggestions,
1148
1247
  updateQuery
1149
1248
  } = search;
1150
- const enabled = agent !== void 0 && isNuiSearchEnabled({ isAuthenticated, flag });
1249
+ const enabled = agent !== void 0 && isNuiSearchEnabled({
1250
+ isAuthenticated,
1251
+ flag,
1252
+ allowGuests: allowGuestAgent
1253
+ });
1151
1254
  const items = getVisibleSuggestions({
1152
1255
  didYouMean,
1153
1256
  suggestions
@@ -1317,6 +1420,22 @@ function CommerceSearchView({
1317
1420
  if (agentQuery !== null)
1318
1421
  navigateTo(service.submitProductSearch(agentQuery));
1319
1422
  }, [agentQuery, navigateTo, service]);
1423
+ const agentProductsProps = agentProducts ? { products: agentProducts } : {};
1424
+ const agentPanel = handoff === null || agent === void 0 ? void 0 : /* @__PURE__ */ jsxRuntime.jsx(
1425
+ NuiAgentPanel,
1426
+ {
1427
+ agent,
1428
+ query: handoff.query,
1429
+ handoffContext: {
1430
+ source: "storefront_search",
1431
+ trigger: "zero_results",
1432
+ query: handoff.query,
1433
+ did_you_mean: handoff.didYouMean
1434
+ },
1435
+ ...agentProductsProps,
1436
+ onUnavailable: handleAgentUnavailable
1437
+ }
1438
+ );
1320
1439
  return /* @__PURE__ */ jsxRuntime.jsx(
1321
1440
  reactSearchComponents.SearchModal,
1322
1441
  {
@@ -1344,20 +1463,7 @@ function CommerceSearchView({
1344
1463
  onSelect: handleRecentSelect,
1345
1464
  onClear: handleClearRecent
1346
1465
  },
1347
- agentPanel: handoff === null || agent === void 0 ? void 0 : /* @__PURE__ */ jsxRuntime.jsx(
1348
- NuiAgentPanel,
1349
- {
1350
- agent,
1351
- query: handoff.query,
1352
- handoffContext: {
1353
- source: "storefront_search",
1354
- trigger: "zero_results",
1355
- query: handoff.query,
1356
- did_you_mean: handoff.didYouMean
1357
- },
1358
- onUnavailable: handleAgentUnavailable
1359
- }
1360
- )
1466
+ agentPanel
1361
1467
  }
1362
1468
  );
1363
1469
  }
@@ -1375,7 +1481,7 @@ function CommerceSearch({
1375
1481
  react.useEffect(() => {
1376
1482
  if (isOpen) setHasOpened(true);
1377
1483
  }, [isOpen]);
1378
- const { minimumSearchLength, agent } = useCommerceSearchConfig({
1484
+ const { minimumSearchLength, agent, allowGuestAgent } = useCommerceSearchConfig({
1379
1485
  country,
1380
1486
  language,
1381
1487
  enabled: hasOpened
@@ -1408,6 +1514,14 @@ function CommerceSearch({
1408
1514
  }),
1409
1515
  [country, language, isAuthenticated, accountType, userId]
1410
1516
  );
1517
+ const agentProducts = react.useMemo(() => {
1518
+ const environment = getProductEnvironment();
1519
+ return environment ? {
1520
+ environment,
1521
+ market: country.toUpperCase(),
1522
+ language: language.toLowerCase()
1523
+ } : void 0;
1524
+ }, [country, language]);
1411
1525
  return /* @__PURE__ */ jsxRuntime.jsx(
1412
1526
  CommerceSearchView,
1413
1527
  {
@@ -1417,6 +1531,8 @@ function CommerceSearch({
1417
1531
  navigate: navigateToPath,
1418
1532
  isAuthenticated,
1419
1533
  ...agent ? { agent } : {},
1534
+ ...allowGuestAgent ? { allowGuestAgent } : {},
1535
+ ...agentProducts ? { agentProducts } : {},
1420
1536
  logo,
1421
1537
  minimumSearchLength,
1422
1538
  gtmContext