@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/dist/index.mjs CHANGED
@@ -1,6 +1,8 @@
1
- import { lazy, useState, useEffect, useRef, useMemo, useCallback, Suspense, Component } from 'react';
1
+ import { lazy, useState, useEffect, useRef, useMemo, useCallback, Component, Suspense } from 'react';
2
2
  import configurationSdk from '@nuskin/configuration-sdk';
3
3
  import { SearchModal } from '@nuskin/react-search-components';
4
+ import { createProductClient, ProductProvider, useProductById, ProductCard } from '@nuskin/react-product-sdk';
5
+ import styled from '@emotion/styled';
4
6
  import { jsx } from 'react/jsx-runtime';
5
7
 
6
8
  // src/components/CommerceSearch/CommerceSearch.tsx
@@ -28,7 +30,10 @@ var TEST_ENVIRONMENT_VALUES = {
28
30
  agent: {
29
31
  appKey: "pk_commerce_product_advisor_624aea4305b09240e8c8f41d0f6527b2",
30
32
  apiBaseUrl: "https://nui.tst.nuskin.ai/"
31
- }
33
+ },
34
+ // Open to visitors who are not signed in, so the handoff can be tried where signing in is not
35
+ // possible (for example local development). Sign-in stays required everywhere else.
36
+ allowGuestAgent: true
32
37
  };
33
38
  function nonEmptyString(value) {
34
39
  return typeof value === "string" ? value.trim() : "";
@@ -45,7 +50,8 @@ function normalizeCommerceSearchConfig(raw, { language } = {}) {
45
50
  apiBaseUrl,
46
51
  ...language ? { language } : {}
47
52
  }
48
- } : {}
53
+ } : {},
54
+ ...raw?.allowGuestAgent === true ? { allowGuestAgent: true } : {}
49
55
  };
50
56
  }
51
57
  async function fetchCommerceSearchConfig({
@@ -299,6 +305,16 @@ function resolveCustomerTypeHeaders(getCustomerType) {
299
305
  const customerType = getCustomerType?.()?.trim();
300
306
  return customerType ? { "X-Customer-Type": customerType } : void 0;
301
307
  }
308
+ var NO_RESULTS_ERROR_CODE = "SFOSER002";
309
+ async function isNoResultsResponse(response) {
310
+ if (response.status !== 404) return false;
311
+ try {
312
+ const body = await response.json();
313
+ return body?.code === NO_RESULTS_ERROR_CODE;
314
+ } catch {
315
+ return false;
316
+ }
317
+ }
302
318
  async function fetchJsonFromCandidates(fetchImpl, requestUrls, requestHeaders = {}) {
303
319
  let lastError;
304
320
  for (const requestUrl of requestUrls) {
@@ -309,6 +325,7 @@ async function fetchJsonFromCandidates(fetchImpl, requestUrls, requestHeaders =
309
325
  headers: { Accept: "application/json", ...requestHeaders }
310
326
  });
311
327
  if (!response.ok) {
328
+ if (await isNoResultsResponse(response)) return null;
312
329
  lastError = new Error(
313
330
  `Request failed with status ${response.status}`
314
331
  );
@@ -475,6 +492,19 @@ function toNavigationTarget(path) {
475
492
  return /^https?:\/\//i.test(path) ? { type: "external", url: path } : { type: "internal", path };
476
493
  }
477
494
 
495
+ // src/utils/productEnvironment.ts
496
+ var PRODUCT_ENVIRONMENTS = ["dev", "test", "prod"];
497
+ function toProductEnvironment(environmentCode) {
498
+ return environmentCode && PRODUCT_ENVIRONMENTS.includes(environmentCode) ? environmentCode : void 0;
499
+ }
500
+ function getProductEnvironment() {
501
+ try {
502
+ return toProductEnvironment(getEnvironmentCode());
503
+ } catch {
504
+ return void 0;
505
+ }
506
+ }
507
+
478
508
  // src/utils/catalogHelpers.ts
479
509
  var CART_DISABLED_PRODUCT_STATUSES = /* @__PURE__ */ new Set([
480
510
  "discontinued",
@@ -1004,9 +1034,10 @@ function resolveNuiSearchFlag(env = {}) {
1004
1034
  }
1005
1035
  function isNuiSearchEnabled({
1006
1036
  isAuthenticated,
1007
- flag
1037
+ flag,
1038
+ allowGuests = false
1008
1039
  }) {
1009
- return isAuthenticated === true && flag === true;
1040
+ return flag === true && (isAuthenticated === true || allowGuests);
1010
1041
  }
1011
1042
 
1012
1043
  // src/utils/searchRouting.ts
@@ -1020,15 +1051,69 @@ function decideSearchRoute({
1020
1051
  if (snapshot.suggestionFetchFailed) return "search";
1021
1052
  return snapshot.suggestions.length > 0 ? "search" : "handoff";
1022
1053
  }
1054
+
1055
+ // src/utils/cachedProductClient.ts
1056
+ function withProductCache(client) {
1057
+ const lookups = /* @__PURE__ */ new Map();
1058
+ return {
1059
+ getProductById(productId) {
1060
+ let lookup = lookups.get(productId);
1061
+ if (!lookup) {
1062
+ lookup = client.getProductById(productId);
1063
+ lookups.set(productId, lookup);
1064
+ }
1065
+ return lookup;
1066
+ },
1067
+ getProductsByIds: (productIds) => client.getProductsByIds(productIds)
1068
+ };
1069
+ }
1070
+ var AgentProductSlot = styled.div`
1071
+ && {
1072
+ flex: 0 0 240px;
1073
+ overflow: hidden;
1074
+ width: 240px;
1075
+ }
1076
+ `;
1077
+ var SalesAIChatWidget = lazy(
1078
+ () => Promise.all([
1079
+ import('@nu-skin-enterprises/chat-widget'),
1080
+ import('@nu-skin-enterprises/chat-widget/styles.css')
1081
+ ]).then(([mod]) => ({ default: mod.SalesAIChatWidget }))
1082
+ );
1023
1083
  var HANDLE_POLL_MS = 50;
1024
1084
  var HANDLE_TIMEOUT_MS = 15e3;
1025
- var SalesAIChatWidget = lazy(async () => {
1026
- const [widget] = await Promise.all([
1027
- import('@nuskin/react-nui-chat-sdk'),
1028
- import('@nuskin/react-nui-chat-sdk/styles.css')
1029
- ]);
1030
- return { default: widget.SalesAIChatWidget };
1031
- });
1085
+ function AgentProductCard({ sku }) {
1086
+ const { product, isLoading, error } = useProductById(sku);
1087
+ if (!isLoading && (error || !product?.seoInformation?.canonicalURL)) {
1088
+ return null;
1089
+ }
1090
+ return /* @__PURE__ */ jsx(AgentProductSlot, { children: /* @__PURE__ */ jsx(ProductCard, { productId: sku }) });
1091
+ }
1092
+ var renderAgentProduct = (product) => /* @__PURE__ */ jsx(AgentProductCard, { sku: product.sku });
1093
+ function AgentWidget({
1094
+ widgetProps,
1095
+ products
1096
+ }) {
1097
+ const environment = products?.environment;
1098
+ const market = products?.market;
1099
+ const language = products?.language;
1100
+ const client = useMemo(
1101
+ () => environment && market && language ? withProductCache(
1102
+ createProductClient({ environment, market, language })
1103
+ ) : void 0,
1104
+ [environment, market, language]
1105
+ );
1106
+ if (!client) {
1107
+ return /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(SalesAIChatWidget, { ...widgetProps }) });
1108
+ }
1109
+ return /* @__PURE__ */ jsx(ProductProvider, { client, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(
1110
+ SalesAIChatWidget,
1111
+ {
1112
+ renderProduct: renderAgentProduct,
1113
+ ...widgetProps
1114
+ }
1115
+ ) }) });
1116
+ }
1032
1117
  function getSalesAIHandle() {
1033
1118
  return window.SalesAI;
1034
1119
  }
@@ -1084,6 +1169,7 @@ function NuiAgentPanel({
1084
1169
  agent,
1085
1170
  query,
1086
1171
  handoffContext,
1172
+ products,
1087
1173
  onUnavailable
1088
1174
  }) {
1089
1175
  useHandoff({
@@ -1092,19 +1178,22 @@ function NuiAgentPanel({
1092
1178
  agentContext: agent.context,
1093
1179
  onUnavailable
1094
1180
  });
1095
- return /* @__PURE__ */ jsx(AgentErrorBoundary, { onError: onUnavailable, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(
1096
- SalesAIChatWidget,
1181
+ const widgetProps = {
1182
+ appKey: agent.appKey,
1183
+ apiBaseUrl: agent.apiBaseUrl,
1184
+ theme: agent.theme ?? "nui-light",
1185
+ defaultOpen: true,
1186
+ hideFab: true,
1187
+ placement: "host",
1188
+ ...agent.language === void 0 ? {} : { language: agent.language }
1189
+ };
1190
+ return /* @__PURE__ */ jsx(AgentErrorBoundary, { onError: onUnavailable, children: /* @__PURE__ */ jsx(
1191
+ AgentWidget,
1097
1192
  {
1098
- appKey: agent.appKey,
1099
- apiBaseUrl: agent.apiBaseUrl,
1100
- theme: agent.theme ?? "nui-light",
1101
- defaultOpen: true,
1102
- hideFab: true,
1103
- placement: "host",
1104
- ...agent.language === void 0 ? {} : { language: agent.language },
1105
- ...agent.renderProduct === void 0 ? {} : { renderProduct: agent.renderProduct }
1193
+ widgetProps,
1194
+ ...products ? { products } : {}
1106
1195
  }
1107
- ) }) });
1196
+ ) });
1108
1197
  }
1109
1198
  var SUBMIT_WAIT_TIMEOUT_MS = 2e3;
1110
1199
  function uniqueRecentSearches(queries) {
@@ -1123,6 +1212,8 @@ function CommerceSearchView({
1123
1212
  navigate,
1124
1213
  isAuthenticated,
1125
1214
  agent,
1215
+ allowGuestAgent = false,
1216
+ agentProducts,
1126
1217
  logo,
1127
1218
  minimumSearchLength = MINIMUM_SEARCH_CHAR_LENGTH,
1128
1219
  gtmContext
@@ -1148,7 +1239,11 @@ function CommerceSearchView({
1148
1239
  suggestions,
1149
1240
  updateQuery
1150
1241
  } = search;
1151
- const enabled = agent !== void 0 && isNuiSearchEnabled({ isAuthenticated, flag });
1242
+ const enabled = agent !== void 0 && isNuiSearchEnabled({
1243
+ isAuthenticated,
1244
+ flag,
1245
+ allowGuests: allowGuestAgent
1246
+ });
1152
1247
  const items = getVisibleSuggestions({
1153
1248
  didYouMean,
1154
1249
  suggestions
@@ -1318,6 +1413,22 @@ function CommerceSearchView({
1318
1413
  if (agentQuery !== null)
1319
1414
  navigateTo(service.submitProductSearch(agentQuery));
1320
1415
  }, [agentQuery, navigateTo, service]);
1416
+ const agentProductsProps = agentProducts ? { products: agentProducts } : {};
1417
+ const agentPanel = handoff === null || agent === void 0 ? void 0 : /* @__PURE__ */ jsx(
1418
+ NuiAgentPanel,
1419
+ {
1420
+ agent,
1421
+ query: handoff.query,
1422
+ handoffContext: {
1423
+ source: "storefront_search",
1424
+ trigger: "zero_results",
1425
+ query: handoff.query,
1426
+ did_you_mean: handoff.didYouMean
1427
+ },
1428
+ ...agentProductsProps,
1429
+ onUnavailable: handleAgentUnavailable
1430
+ }
1431
+ );
1321
1432
  return /* @__PURE__ */ jsx(
1322
1433
  SearchModal,
1323
1434
  {
@@ -1345,20 +1456,7 @@ function CommerceSearchView({
1345
1456
  onSelect: handleRecentSelect,
1346
1457
  onClear: handleClearRecent
1347
1458
  },
1348
- agentPanel: handoff === null || agent === void 0 ? void 0 : /* @__PURE__ */ jsx(
1349
- NuiAgentPanel,
1350
- {
1351
- agent,
1352
- query: handoff.query,
1353
- handoffContext: {
1354
- source: "storefront_search",
1355
- trigger: "zero_results",
1356
- query: handoff.query,
1357
- did_you_mean: handoff.didYouMean
1358
- },
1359
- onUnavailable: handleAgentUnavailable
1360
- }
1361
- )
1459
+ agentPanel
1362
1460
  }
1363
1461
  );
1364
1462
  }
@@ -1376,7 +1474,7 @@ function CommerceSearch({
1376
1474
  useEffect(() => {
1377
1475
  if (isOpen) setHasOpened(true);
1378
1476
  }, [isOpen]);
1379
- const { minimumSearchLength, agent } = useCommerceSearchConfig({
1477
+ const { minimumSearchLength, agent, allowGuestAgent } = useCommerceSearchConfig({
1380
1478
  country,
1381
1479
  language,
1382
1480
  enabled: hasOpened
@@ -1409,6 +1507,14 @@ function CommerceSearch({
1409
1507
  }),
1410
1508
  [country, language, isAuthenticated, accountType, userId]
1411
1509
  );
1510
+ const agentProducts = useMemo(() => {
1511
+ const environment = getProductEnvironment();
1512
+ return environment ? {
1513
+ environment,
1514
+ market: country.toUpperCase(),
1515
+ language: language.toLowerCase()
1516
+ } : void 0;
1517
+ }, [country, language]);
1412
1518
  return /* @__PURE__ */ jsx(
1413
1519
  CommerceSearchView,
1414
1520
  {
@@ -1418,6 +1524,8 @@ function CommerceSearch({
1418
1524
  navigate: navigateToPath,
1419
1525
  isAuthenticated,
1420
1526
  ...agent ? { agent } : {},
1527
+ ...allowGuestAgent ? { allowGuestAgent } : {},
1528
+ ...agentProducts ? { agentProducts } : {},
1421
1529
  logo,
1422
1530
  minimumSearchLength,
1423
1531
  gtmContext