@nuskin/react-search-sdk 1.0.0-trying-again.3 → 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,7 +7,28 @@ 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
 
@@ -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
  );
@@ -481,6 +499,19 @@ function toNavigationTarget(path) {
481
499
  return /^https?:\/\//i.test(path) ? { type: "external", url: path } : { type: "internal", path };
482
500
  }
483
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
+
484
515
  // src/utils/catalogHelpers.ts
485
516
  var CART_DISABLED_PRODUCT_STATUSES = /* @__PURE__ */ new Set([
486
517
  "discontinued",
@@ -1010,9 +1041,10 @@ function resolveNuiSearchFlag(env = {}) {
1010
1041
  }
1011
1042
  function isNuiSearchEnabled({
1012
1043
  isAuthenticated,
1013
- flag
1044
+ flag,
1045
+ allowGuests = false
1014
1046
  }) {
1015
- return isAuthenticated === true && flag === true;
1047
+ return flag === true && (isAuthenticated === true || allowGuests);
1016
1048
  }
1017
1049
 
1018
1050
  // src/utils/searchRouting.ts
@@ -1026,15 +1058,69 @@ function decideSearchRoute({
1026
1058
  if (snapshot.suggestionFetchFailed) return "search";
1027
1059
  return snapshot.suggestions.length > 0 ? "search" : "handoff";
1028
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
+ );
1029
1090
  var HANDLE_POLL_MS = 50;
1030
1091
  var HANDLE_TIMEOUT_MS = 15e3;
1031
- var SalesAIChatWidget = react.lazy(async () => {
1032
- const [widget] = await Promise.all([
1033
- import('@nuskin/react-nui-chat-sdk'),
1034
- import('@nuskin/react-nui-chat-sdk/styles.css')
1035
- ]);
1036
- return { default: widget.SalesAIChatWidget };
1037
- });
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
+ }
1038
1124
  function getSalesAIHandle() {
1039
1125
  return window.SalesAI;
1040
1126
  }
@@ -1090,6 +1176,7 @@ function NuiAgentPanel({
1090
1176
  agent,
1091
1177
  query,
1092
1178
  handoffContext,
1179
+ products,
1093
1180
  onUnavailable
1094
1181
  }) {
1095
1182
  useHandoff({
@@ -1098,19 +1185,22 @@ function NuiAgentPanel({
1098
1185
  agentContext: agent.context,
1099
1186
  onUnavailable
1100
1187
  });
1101
- return /* @__PURE__ */ jsxRuntime.jsx(AgentErrorBoundary, { onError: onUnavailable, children: /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(
1102
- 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,
1103
1199
  {
1104
- appKey: agent.appKey,
1105
- apiBaseUrl: agent.apiBaseUrl,
1106
- theme: agent.theme ?? "nui-light",
1107
- defaultOpen: true,
1108
- hideFab: true,
1109
- placement: "host",
1110
- ...agent.language === void 0 ? {} : { language: agent.language },
1111
- ...agent.renderProduct === void 0 ? {} : { renderProduct: agent.renderProduct }
1200
+ widgetProps,
1201
+ ...products ? { products } : {}
1112
1202
  }
1113
- ) }) });
1203
+ ) });
1114
1204
  }
1115
1205
  var SUBMIT_WAIT_TIMEOUT_MS = 2e3;
1116
1206
  function uniqueRecentSearches(queries) {
@@ -1129,6 +1219,8 @@ function CommerceSearchView({
1129
1219
  navigate,
1130
1220
  isAuthenticated,
1131
1221
  agent,
1222
+ allowGuestAgent = false,
1223
+ agentProducts,
1132
1224
  logo,
1133
1225
  minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH,
1134
1226
  gtmContext
@@ -1154,7 +1246,11 @@ function CommerceSearchView({
1154
1246
  suggestions,
1155
1247
  updateQuery
1156
1248
  } = search;
1157
- const enabled = agent !== void 0 && isNuiSearchEnabled({ isAuthenticated, flag });
1249
+ const enabled = agent !== void 0 && isNuiSearchEnabled({
1250
+ isAuthenticated,
1251
+ flag,
1252
+ allowGuests: allowGuestAgent
1253
+ });
1158
1254
  const items = getVisibleSuggestions({
1159
1255
  didYouMean,
1160
1256
  suggestions
@@ -1324,6 +1420,22 @@ function CommerceSearchView({
1324
1420
  if (agentQuery !== null)
1325
1421
  navigateTo(service.submitProductSearch(agentQuery));
1326
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
+ );
1327
1439
  return /* @__PURE__ */ jsxRuntime.jsx(
1328
1440
  reactSearchComponents.SearchModal,
1329
1441
  {
@@ -1351,20 +1463,7 @@ function CommerceSearchView({
1351
1463
  onSelect: handleRecentSelect,
1352
1464
  onClear: handleClearRecent
1353
1465
  },
1354
- agentPanel: handoff === null || agent === void 0 ? void 0 : /* @__PURE__ */ jsxRuntime.jsx(
1355
- NuiAgentPanel,
1356
- {
1357
- agent,
1358
- query: handoff.query,
1359
- handoffContext: {
1360
- source: "storefront_search",
1361
- trigger: "zero_results",
1362
- query: handoff.query,
1363
- did_you_mean: handoff.didYouMean
1364
- },
1365
- onUnavailable: handleAgentUnavailable
1366
- }
1367
- )
1466
+ agentPanel
1368
1467
  }
1369
1468
  );
1370
1469
  }
@@ -1382,7 +1481,7 @@ function CommerceSearch({
1382
1481
  react.useEffect(() => {
1383
1482
  if (isOpen) setHasOpened(true);
1384
1483
  }, [isOpen]);
1385
- const { minimumSearchLength, agent } = useCommerceSearchConfig({
1484
+ const { minimumSearchLength, agent, allowGuestAgent } = useCommerceSearchConfig({
1386
1485
  country,
1387
1486
  language,
1388
1487
  enabled: hasOpened
@@ -1415,6 +1514,14 @@ function CommerceSearch({
1415
1514
  }),
1416
1515
  [country, language, isAuthenticated, accountType, userId]
1417
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]);
1418
1525
  return /* @__PURE__ */ jsxRuntime.jsx(
1419
1526
  CommerceSearchView,
1420
1527
  {
@@ -1424,6 +1531,8 @@ function CommerceSearch({
1424
1531
  navigate: navigateToPath,
1425
1532
  isAuthenticated,
1426
1533
  ...agent ? { agent } : {},
1534
+ ...allowGuestAgent ? { allowGuestAgent } : {},
1535
+ ...agentProducts ? { agentProducts } : {},
1427
1536
  logo,
1428
1537
  minimumSearchLength,
1429
1538
  gtmContext