@blotoutio/providers-evo-search-sdk 1.55.2 → 1.56.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/core.cjs.js CHANGED
@@ -511,6 +511,15 @@ new Set([
511
511
  OFFLINE_TOUCH,
512
512
  ]);
513
513
 
514
+ const CURRENCY_CODE_PATTERN = /^[A-Z]{3}$/;
515
+ const parseCurrencyCode = (value) => {
516
+ if (typeof value !== 'string') {
517
+ return undefined;
518
+ }
519
+ const code = value.trim().toUpperCase();
520
+ return CURRENCY_CODE_PATTERN.test(code) ? code : undefined;
521
+ };
522
+
514
523
  const uiActions = new Set([
515
524
  'evoSearchProductRecommendationClicked',
516
525
  'evoSearchProductClicked',
@@ -601,7 +610,24 @@ const notifyReady = () => {
601
610
  notifySubscribers(registry.subscribers, 'notifyReady');
602
611
  };
603
612
 
604
- const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId, sessionId, sendTag, }) => {
613
+ // Always defer to what the storefront itself is rendering same source
614
+ // shop-gpt reads from. Two widgets on the same page would otherwise drift:
615
+ // shop-gpt picks Shopify, evo-search would pick its own override and show
616
+ // the same product in a different currency. `uiPreferences.currency` is
617
+ // kept as a last-resort fallback for non-Shopify hosts; 'USD' is the
618
+ // final default to match shop-gpt's behaviour.
619
+ const readShopifyCurrency = () => {
620
+ var _a;
621
+ try {
622
+ const shopify = window.Shopify;
623
+ return parseCurrencyCode((_a = shopify === null || shopify === void 0 ? void 0 : shopify.currency) === null || _a === void 0 ? void 0 : _a.active);
624
+ }
625
+ catch {
626
+ return undefined;
627
+ }
628
+ };
629
+ const resolveDisplayCurrency = (uiPreferences) => { var _a, _b; return (_b = (_a = readShopifyCurrency()) !== null && _a !== void 0 ? _a : parseCurrencyCode(uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.currency)) !== null && _b !== void 0 ? _b : 'USD'; };
630
+ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId, sessionId, sendTag, uiPreferences, }) => {
605
631
  if (!baseURL) {
606
632
  throw new Error('baseURL missing');
607
633
  }
@@ -618,62 +644,82 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
618
644
  }
619
645
  return headers;
620
646
  };
647
+ // Parsing `baseURL` once at API construction avoids re-parsing the host on
648
+ // every keystroke when `getURL` runs inside the autocomplete hot path.
649
+ const baseOrigin = new URL(baseURL).origin;
650
+ const PROVIDER_PATH_PREFIX = '/providers/evoSearch';
621
651
  const getURL = (path) => {
622
- return new URL(`/providers/evoSearch${path}`, baseURL);
652
+ const url = new URL(baseOrigin);
653
+ url.pathname = `${PROVIDER_PATH_PREFIX}${path}`;
654
+ url.searchParams.set('currency', resolveDisplayCurrency(uiPreferences));
655
+ return url;
623
656
  };
624
- const autocomplete = async (query, limit = 8, signal) => {
625
- const url = getURL('/autocomplete');
626
- url.searchParams.set('q', query);
627
- url.searchParams.set('limit', limit.toString());
628
- const headers = getHeaders();
629
- logger.info(`EvoSearch API: Autocomplete request - query length: ${query.length}, limit: ${limit}`);
657
+ // Only `/user/*` endpoints return per-user PII (recents, profile-bound
658
+ // data) and rely on the worker's HMAC-signed cookie. Sending
659
+ // `credentials: 'include'` on the public read endpoints (autocomplete,
660
+ // search, trending) is pointless and triggers the strict CORS contract
661
+ // (`Allow-Credentials: true` + non-wildcard origin echo) on every
662
+ // response so default to `omit` and opt in below.
663
+ const needsCredentials = (path) => path.startsWith('/user/');
664
+ const getJSON = async (path, label, params, signal) => {
665
+ const url = getURL(path);
666
+ for (const [key, value] of Object.entries(params !== null && params !== void 0 ? params : {})) {
667
+ url.searchParams.set(key, value);
668
+ }
630
669
  const response = await fetchImpl(url, {
631
670
  method: 'GET',
632
- headers,
633
- credentials: 'include',
671
+ headers: getHeaders(),
672
+ credentials: needsCredentials(path) ? 'include' : 'omit',
634
673
  signal,
635
674
  });
636
675
  if (!response.ok) {
637
- const errorText = await response.text();
638
- logger.error(`EvoSearch API: Autocomplete failed - ${response.status}: ${errorText}`);
639
- throw new Error(`Autocomplete failed - ${response.status}`);
676
+ const errorText = await response.text().catch(() => '');
677
+ logger.error(`EvoSearch API: ${label} failed - ${response.status}: ${errorText}`);
678
+ throw new Error(`${label} failed - ${response.status}`);
640
679
  }
641
- const data = (await response.json());
680
+ return (await response.json());
681
+ };
682
+ const autocomplete = async (query, limit = 8, signal) => {
683
+ logger.info(`EvoSearch API: Autocomplete request - query length: ${query.length}, limit: ${limit}`);
684
+ const data = await getJSON('/autocomplete', 'Autocomplete', { q: query, limit: String(limit) }, signal);
642
685
  logger.info(`EvoSearch API: Autocomplete success - ${data.llmSuggestions.length} suggestions, ${data.products.length} products`);
643
686
  return data;
644
687
  };
645
- const trending = async (signal) => {
646
- const url = getURL('/trending');
688
+ const trending = (signal) => {
647
689
  logger.info('EvoSearch API: Fetching trending searches');
648
- const response = await fetchImpl(url, {
649
- method: 'GET',
650
- headers: getHeaders(),
651
- credentials: 'include',
652
- signal,
653
- });
654
- if (!response.ok) {
655
- throw new Error(`Trending failed - ${response.status}`);
690
+ return getJSON('/trending', 'Trending', undefined, signal);
691
+ };
692
+ const recents = (signal) => {
693
+ logger.info('EvoSearch API: Fetching recent searches');
694
+ return getJSON('/user/recents', 'Recents', undefined, signal);
695
+ };
696
+ const sendMutation = async (path, label, method, params) => {
697
+ const url = getURL(path);
698
+ for (const [key, value] of Object.entries(params !== null && params !== void 0 ? params : {})) {
699
+ url.searchParams.set(key, value);
700
+ }
701
+ try {
702
+ const response = await fetchImpl(url, {
703
+ method,
704
+ headers: getHeaders(),
705
+ credentials: 'include',
706
+ });
707
+ if (!response.ok) {
708
+ const errorText = await response.text().catch(() => '');
709
+ logger.error(`EvoSearch API: ${label} failed - ${response.status}: ${errorText}`);
710
+ throw new Error(`${label} failed - ${response.status}`);
711
+ }
712
+ }
713
+ catch (error) {
714
+ logger.error(`EvoSearch API: ${label} threw`, error);
715
+ throw error;
656
716
  }
657
- return await response.json();
658
717
  };
718
+ const deleteRecent = (value) => sendMutation('/user/recents', 'DeleteRecent', 'DELETE', { value });
719
+ const clearRecents = () => sendMutation('/user/recents', 'ClearRecents', 'DELETE');
659
720
  const search = async (query, limit = 8, signal) => {
660
- const url = getURL('/search');
661
- url.searchParams.set('q', query);
662
- url.searchParams.set('limit', limit.toString());
663
- const headers = getHeaders();
664
721
  logger.info(`EvoSearch API: Search request - query length: ${query.length}, limit: ${limit}`);
665
- const response = await fetchImpl(url, {
666
- method: 'GET',
667
- headers,
668
- credentials: 'include',
669
- signal,
670
- });
671
- if (!response.ok) {
672
- const errorText = await response.text();
673
- logger.error(`EvoSearch API: Search failed - ${response.status}: ${errorText}`);
674
- throw new Error(`Search failed - ${response.status}`);
675
- }
676
- const data = (await response.json());
722
+ const data = await getJSON('/search', 'Search', { q: query, limit: String(limit) }, signal);
677
723
  logger.info(`EvoSearch API: Search success - ${data.results.length} products`);
678
724
  return data;
679
725
  };
@@ -720,6 +766,11 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
720
766
  return {
721
767
  autocomplete,
722
768
  trending,
769
+ user: {
770
+ recents,
771
+ deleteRecent,
772
+ clearRecents,
773
+ },
723
774
  search,
724
775
  sendEvent,
725
776
  };
@@ -734,11 +785,20 @@ const init = (params) => {
734
785
  try {
735
786
  (_a = window[registryKey]) !== null && _a !== void 0 ? _a : (window[registryKey] = {});
736
787
  logger.info('EvoSearch SDK: Initializing');
788
+ if (new URLSearchParams(window.location.search).get('evs') === 'preview') {
789
+ try {
790
+ sessionStorage.setItem(previewKeyName, '1');
791
+ logger.info('EvoSearch SDK: Preview mode enabled via ?evs=preview URL param');
792
+ }
793
+ catch {
794
+ error('EvoSearch SDK: Failed to set previewEvoSearch sessionStorage');
795
+ }
796
+ }
737
797
  const { enabled, uiPreferences } = (_c = (_b = params.manifest) === null || _b === void 0 ? void 0 : _b.variables) !== null && _c !== void 0 ? _c : {};
738
798
  const mode = uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.mode;
739
799
  const hasPreview = hasPreviewKey();
740
800
  const shouldEnable = enabled || hasPreview;
741
- if (!shouldEnable || (mode === 'disabled' && !hasPreview)) {
801
+ if (!shouldEnable) {
742
802
  logger.info('EvoSearch SDK: Mode is disabled, skipping initialization');
743
803
  return;
744
804
  }
@@ -750,6 +810,7 @@ const init = (params) => {
750
810
  userId: params.userId,
751
811
  sessionId: (_d = params.session) === null || _d === void 0 ? void 0 : _d.sessionId,
752
812
  sendTag: params.sendTag,
813
+ uiPreferences,
753
814
  });
754
815
  window[registryKey].api = evoSearchAPI;
755
816
  const uiImplementation = window[registryKey].ui;
package/core.js CHANGED
@@ -512,6 +512,15 @@ var ProvidersEvoSearchSdk = (function () {
512
512
  OFFLINE_TOUCH,
513
513
  ]);
514
514
 
515
+ const CURRENCY_CODE_PATTERN = /^[A-Z]{3}$/;
516
+ const parseCurrencyCode = (value) => {
517
+ if (typeof value !== 'string') {
518
+ return undefined;
519
+ }
520
+ const code = value.trim().toUpperCase();
521
+ return CURRENCY_CODE_PATTERN.test(code) ? code : undefined;
522
+ };
523
+
515
524
  const uiActions = new Set([
516
525
  'evoSearchProductRecommendationClicked',
517
526
  'evoSearchProductClicked',
@@ -602,7 +611,24 @@ var ProvidersEvoSearchSdk = (function () {
602
611
  notifySubscribers(registry.subscribers, 'notifyReady');
603
612
  };
604
613
 
605
- const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId, sessionId, sendTag, }) => {
614
+ // Always defer to what the storefront itself is rendering same source
615
+ // shop-gpt reads from. Two widgets on the same page would otherwise drift:
616
+ // shop-gpt picks Shopify, evo-search would pick its own override and show
617
+ // the same product in a different currency. `uiPreferences.currency` is
618
+ // kept as a last-resort fallback for non-Shopify hosts; 'USD' is the
619
+ // final default to match shop-gpt's behaviour.
620
+ const readShopifyCurrency = () => {
621
+ var _a;
622
+ try {
623
+ const shopify = window.Shopify;
624
+ return parseCurrencyCode((_a = shopify === null || shopify === void 0 ? void 0 : shopify.currency) === null || _a === void 0 ? void 0 : _a.active);
625
+ }
626
+ catch {
627
+ return undefined;
628
+ }
629
+ };
630
+ const resolveDisplayCurrency = (uiPreferences) => { var _a, _b; return (_b = (_a = readShopifyCurrency()) !== null && _a !== void 0 ? _a : parseCurrencyCode(uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.currency)) !== null && _b !== void 0 ? _b : 'USD'; };
631
+ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId, sessionId, sendTag, uiPreferences, }) => {
606
632
  if (!baseURL) {
607
633
  throw new Error('baseURL missing');
608
634
  }
@@ -619,62 +645,82 @@ var ProvidersEvoSearchSdk = (function () {
619
645
  }
620
646
  return headers;
621
647
  };
648
+ // Parsing `baseURL` once at API construction avoids re-parsing the host on
649
+ // every keystroke when `getURL` runs inside the autocomplete hot path.
650
+ const baseOrigin = new URL(baseURL).origin;
651
+ const PROVIDER_PATH_PREFIX = '/providers/evoSearch';
622
652
  const getURL = (path) => {
623
- return new URL(`/providers/evoSearch${path}`, baseURL);
653
+ const url = new URL(baseOrigin);
654
+ url.pathname = `${PROVIDER_PATH_PREFIX}${path}`;
655
+ url.searchParams.set('currency', resolveDisplayCurrency(uiPreferences));
656
+ return url;
624
657
  };
625
- const autocomplete = async (query, limit = 8, signal) => {
626
- const url = getURL('/autocomplete');
627
- url.searchParams.set('q', query);
628
- url.searchParams.set('limit', limit.toString());
629
- const headers = getHeaders();
630
- logger.info(`EvoSearch API: Autocomplete request - query length: ${query.length}, limit: ${limit}`);
658
+ // Only `/user/*` endpoints return per-user PII (recents, profile-bound
659
+ // data) and rely on the worker's HMAC-signed cookie. Sending
660
+ // `credentials: 'include'` on the public read endpoints (autocomplete,
661
+ // search, trending) is pointless and triggers the strict CORS contract
662
+ // (`Allow-Credentials: true` + non-wildcard origin echo) on every
663
+ // response so default to `omit` and opt in below.
664
+ const needsCredentials = (path) => path.startsWith('/user/');
665
+ const getJSON = async (path, label, params, signal) => {
666
+ const url = getURL(path);
667
+ for (const [key, value] of Object.entries(params !== null && params !== void 0 ? params : {})) {
668
+ url.searchParams.set(key, value);
669
+ }
631
670
  const response = await fetchImpl(url, {
632
671
  method: 'GET',
633
- headers,
634
- credentials: 'include',
672
+ headers: getHeaders(),
673
+ credentials: needsCredentials(path) ? 'include' : 'omit',
635
674
  signal,
636
675
  });
637
676
  if (!response.ok) {
638
- const errorText = await response.text();
639
- logger.error(`EvoSearch API: Autocomplete failed - ${response.status}: ${errorText}`);
640
- throw new Error(`Autocomplete failed - ${response.status}`);
677
+ const errorText = await response.text().catch(() => '');
678
+ logger.error(`EvoSearch API: ${label} failed - ${response.status}: ${errorText}`);
679
+ throw new Error(`${label} failed - ${response.status}`);
641
680
  }
642
- const data = (await response.json());
681
+ return (await response.json());
682
+ };
683
+ const autocomplete = async (query, limit = 8, signal) => {
684
+ logger.info(`EvoSearch API: Autocomplete request - query length: ${query.length}, limit: ${limit}`);
685
+ const data = await getJSON('/autocomplete', 'Autocomplete', { q: query, limit: String(limit) }, signal);
643
686
  logger.info(`EvoSearch API: Autocomplete success - ${data.llmSuggestions.length} suggestions, ${data.products.length} products`);
644
687
  return data;
645
688
  };
646
- const trending = async (signal) => {
647
- const url = getURL('/trending');
689
+ const trending = (signal) => {
648
690
  logger.info('EvoSearch API: Fetching trending searches');
649
- const response = await fetchImpl(url, {
650
- method: 'GET',
651
- headers: getHeaders(),
652
- credentials: 'include',
653
- signal,
654
- });
655
- if (!response.ok) {
656
- throw new Error(`Trending failed - ${response.status}`);
691
+ return getJSON('/trending', 'Trending', undefined, signal);
692
+ };
693
+ const recents = (signal) => {
694
+ logger.info('EvoSearch API: Fetching recent searches');
695
+ return getJSON('/user/recents', 'Recents', undefined, signal);
696
+ };
697
+ const sendMutation = async (path, label, method, params) => {
698
+ const url = getURL(path);
699
+ for (const [key, value] of Object.entries(params !== null && params !== void 0 ? params : {})) {
700
+ url.searchParams.set(key, value);
701
+ }
702
+ try {
703
+ const response = await fetchImpl(url, {
704
+ method,
705
+ headers: getHeaders(),
706
+ credentials: 'include',
707
+ });
708
+ if (!response.ok) {
709
+ const errorText = await response.text().catch(() => '');
710
+ logger.error(`EvoSearch API: ${label} failed - ${response.status}: ${errorText}`);
711
+ throw new Error(`${label} failed - ${response.status}`);
712
+ }
713
+ }
714
+ catch (error) {
715
+ logger.error(`EvoSearch API: ${label} threw`, error);
716
+ throw error;
657
717
  }
658
- return await response.json();
659
718
  };
719
+ const deleteRecent = (value) => sendMutation('/user/recents', 'DeleteRecent', 'DELETE', { value });
720
+ const clearRecents = () => sendMutation('/user/recents', 'ClearRecents', 'DELETE');
660
721
  const search = async (query, limit = 8, signal) => {
661
- const url = getURL('/search');
662
- url.searchParams.set('q', query);
663
- url.searchParams.set('limit', limit.toString());
664
- const headers = getHeaders();
665
722
  logger.info(`EvoSearch API: Search request - query length: ${query.length}, limit: ${limit}`);
666
- const response = await fetchImpl(url, {
667
- method: 'GET',
668
- headers,
669
- credentials: 'include',
670
- signal,
671
- });
672
- if (!response.ok) {
673
- const errorText = await response.text();
674
- logger.error(`EvoSearch API: Search failed - ${response.status}: ${errorText}`);
675
- throw new Error(`Search failed - ${response.status}`);
676
- }
677
- const data = (await response.json());
723
+ const data = await getJSON('/search', 'Search', { q: query, limit: String(limit) }, signal);
678
724
  logger.info(`EvoSearch API: Search success - ${data.results.length} products`);
679
725
  return data;
680
726
  };
@@ -721,6 +767,11 @@ var ProvidersEvoSearchSdk = (function () {
721
767
  return {
722
768
  autocomplete,
723
769
  trending,
770
+ user: {
771
+ recents,
772
+ deleteRecent,
773
+ clearRecents,
774
+ },
724
775
  search,
725
776
  sendEvent,
726
777
  };
@@ -735,11 +786,20 @@ var ProvidersEvoSearchSdk = (function () {
735
786
  try {
736
787
  (_a = window[registryKey]) !== null && _a !== void 0 ? _a : (window[registryKey] = {});
737
788
  logger.info('EvoSearch SDK: Initializing');
789
+ if (new URLSearchParams(window.location.search).get('evs') === 'preview') {
790
+ try {
791
+ sessionStorage.setItem(previewKeyName, '1');
792
+ logger.info('EvoSearch SDK: Preview mode enabled via ?evs=preview URL param');
793
+ }
794
+ catch {
795
+ error('EvoSearch SDK: Failed to set previewEvoSearch sessionStorage');
796
+ }
797
+ }
738
798
  const { enabled, uiPreferences } = (_c = (_b = params.manifest) === null || _b === void 0 ? void 0 : _b.variables) !== null && _c !== void 0 ? _c : {};
739
799
  const mode = uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.mode;
740
800
  const hasPreview = hasPreviewKey();
741
801
  const shouldEnable = enabled || hasPreview;
742
- if (!shouldEnable || (mode === 'disabled' && !hasPreview)) {
802
+ if (!shouldEnable) {
743
803
  logger.info('EvoSearch SDK: Mode is disabled, skipping initialization');
744
804
  return;
745
805
  }
@@ -751,6 +811,7 @@ var ProvidersEvoSearchSdk = (function () {
751
811
  userId: params.userId,
752
812
  sessionId: (_d = params.session) === null || _d === void 0 ? void 0 : _d.sessionId,
753
813
  sendTag: params.sendTag,
814
+ uiPreferences,
754
815
  });
755
816
  window[registryKey].api = evoSearchAPI;
756
817
  const uiImplementation = window[registryKey].ui;
package/core.mjs CHANGED
@@ -509,6 +509,15 @@ new Set([
509
509
  OFFLINE_TOUCH,
510
510
  ]);
511
511
 
512
+ const CURRENCY_CODE_PATTERN = /^[A-Z]{3}$/;
513
+ const parseCurrencyCode = (value) => {
514
+ if (typeof value !== 'string') {
515
+ return undefined;
516
+ }
517
+ const code = value.trim().toUpperCase();
518
+ return CURRENCY_CODE_PATTERN.test(code) ? code : undefined;
519
+ };
520
+
512
521
  const uiActions = new Set([
513
522
  'evoSearchProductRecommendationClicked',
514
523
  'evoSearchProductClicked',
@@ -599,7 +608,24 @@ const notifyReady = () => {
599
608
  notifySubscribers(registry.subscribers, 'notifyReady');
600
609
  };
601
610
 
602
- const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId, sessionId, sendTag, }) => {
611
+ // Always defer to what the storefront itself is rendering same source
612
+ // shop-gpt reads from. Two widgets on the same page would otherwise drift:
613
+ // shop-gpt picks Shopify, evo-search would pick its own override and show
614
+ // the same product in a different currency. `uiPreferences.currency` is
615
+ // kept as a last-resort fallback for non-Shopify hosts; 'USD' is the
616
+ // final default to match shop-gpt's behaviour.
617
+ const readShopifyCurrency = () => {
618
+ var _a;
619
+ try {
620
+ const shopify = window.Shopify;
621
+ return parseCurrencyCode((_a = shopify === null || shopify === void 0 ? void 0 : shopify.currency) === null || _a === void 0 ? void 0 : _a.active);
622
+ }
623
+ catch {
624
+ return undefined;
625
+ }
626
+ };
627
+ const resolveDisplayCurrency = (uiPreferences) => { var _a, _b; return (_b = (_a = readShopifyCurrency()) !== null && _a !== void 0 ? _a : parseCurrencyCode(uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.currency)) !== null && _b !== void 0 ? _b : 'USD'; };
628
+ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId, sessionId, sendTag, uiPreferences, }) => {
603
629
  if (!baseURL) {
604
630
  throw new Error('baseURL missing');
605
631
  }
@@ -616,62 +642,82 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
616
642
  }
617
643
  return headers;
618
644
  };
645
+ // Parsing `baseURL` once at API construction avoids re-parsing the host on
646
+ // every keystroke when `getURL` runs inside the autocomplete hot path.
647
+ const baseOrigin = new URL(baseURL).origin;
648
+ const PROVIDER_PATH_PREFIX = '/providers/evoSearch';
619
649
  const getURL = (path) => {
620
- return new URL(`/providers/evoSearch${path}`, baseURL);
650
+ const url = new URL(baseOrigin);
651
+ url.pathname = `${PROVIDER_PATH_PREFIX}${path}`;
652
+ url.searchParams.set('currency', resolveDisplayCurrency(uiPreferences));
653
+ return url;
621
654
  };
622
- const autocomplete = async (query, limit = 8, signal) => {
623
- const url = getURL('/autocomplete');
624
- url.searchParams.set('q', query);
625
- url.searchParams.set('limit', limit.toString());
626
- const headers = getHeaders();
627
- logger.info(`EvoSearch API: Autocomplete request - query length: ${query.length}, limit: ${limit}`);
655
+ // Only `/user/*` endpoints return per-user PII (recents, profile-bound
656
+ // data) and rely on the worker's HMAC-signed cookie. Sending
657
+ // `credentials: 'include'` on the public read endpoints (autocomplete,
658
+ // search, trending) is pointless and triggers the strict CORS contract
659
+ // (`Allow-Credentials: true` + non-wildcard origin echo) on every
660
+ // response so default to `omit` and opt in below.
661
+ const needsCredentials = (path) => path.startsWith('/user/');
662
+ const getJSON = async (path, label, params, signal) => {
663
+ const url = getURL(path);
664
+ for (const [key, value] of Object.entries(params !== null && params !== void 0 ? params : {})) {
665
+ url.searchParams.set(key, value);
666
+ }
628
667
  const response = await fetchImpl(url, {
629
668
  method: 'GET',
630
- headers,
631
- credentials: 'include',
669
+ headers: getHeaders(),
670
+ credentials: needsCredentials(path) ? 'include' : 'omit',
632
671
  signal,
633
672
  });
634
673
  if (!response.ok) {
635
- const errorText = await response.text();
636
- logger.error(`EvoSearch API: Autocomplete failed - ${response.status}: ${errorText}`);
637
- throw new Error(`Autocomplete failed - ${response.status}`);
674
+ const errorText = await response.text().catch(() => '');
675
+ logger.error(`EvoSearch API: ${label} failed - ${response.status}: ${errorText}`);
676
+ throw new Error(`${label} failed - ${response.status}`);
638
677
  }
639
- const data = (await response.json());
678
+ return (await response.json());
679
+ };
680
+ const autocomplete = async (query, limit = 8, signal) => {
681
+ logger.info(`EvoSearch API: Autocomplete request - query length: ${query.length}, limit: ${limit}`);
682
+ const data = await getJSON('/autocomplete', 'Autocomplete', { q: query, limit: String(limit) }, signal);
640
683
  logger.info(`EvoSearch API: Autocomplete success - ${data.llmSuggestions.length} suggestions, ${data.products.length} products`);
641
684
  return data;
642
685
  };
643
- const trending = async (signal) => {
644
- const url = getURL('/trending');
686
+ const trending = (signal) => {
645
687
  logger.info('EvoSearch API: Fetching trending searches');
646
- const response = await fetchImpl(url, {
647
- method: 'GET',
648
- headers: getHeaders(),
649
- credentials: 'include',
650
- signal,
651
- });
652
- if (!response.ok) {
653
- throw new Error(`Trending failed - ${response.status}`);
688
+ return getJSON('/trending', 'Trending', undefined, signal);
689
+ };
690
+ const recents = (signal) => {
691
+ logger.info('EvoSearch API: Fetching recent searches');
692
+ return getJSON('/user/recents', 'Recents', undefined, signal);
693
+ };
694
+ const sendMutation = async (path, label, method, params) => {
695
+ const url = getURL(path);
696
+ for (const [key, value] of Object.entries(params !== null && params !== void 0 ? params : {})) {
697
+ url.searchParams.set(key, value);
698
+ }
699
+ try {
700
+ const response = await fetchImpl(url, {
701
+ method,
702
+ headers: getHeaders(),
703
+ credentials: 'include',
704
+ });
705
+ if (!response.ok) {
706
+ const errorText = await response.text().catch(() => '');
707
+ logger.error(`EvoSearch API: ${label} failed - ${response.status}: ${errorText}`);
708
+ throw new Error(`${label} failed - ${response.status}`);
709
+ }
710
+ }
711
+ catch (error) {
712
+ logger.error(`EvoSearch API: ${label} threw`, error);
713
+ throw error;
654
714
  }
655
- return await response.json();
656
715
  };
716
+ const deleteRecent = (value) => sendMutation('/user/recents', 'DeleteRecent', 'DELETE', { value });
717
+ const clearRecents = () => sendMutation('/user/recents', 'ClearRecents', 'DELETE');
657
718
  const search = async (query, limit = 8, signal) => {
658
- const url = getURL('/search');
659
- url.searchParams.set('q', query);
660
- url.searchParams.set('limit', limit.toString());
661
- const headers = getHeaders();
662
719
  logger.info(`EvoSearch API: Search request - query length: ${query.length}, limit: ${limit}`);
663
- const response = await fetchImpl(url, {
664
- method: 'GET',
665
- headers,
666
- credentials: 'include',
667
- signal,
668
- });
669
- if (!response.ok) {
670
- const errorText = await response.text();
671
- logger.error(`EvoSearch API: Search failed - ${response.status}: ${errorText}`);
672
- throw new Error(`Search failed - ${response.status}`);
673
- }
674
- const data = (await response.json());
720
+ const data = await getJSON('/search', 'Search', { q: query, limit: String(limit) }, signal);
675
721
  logger.info(`EvoSearch API: Search success - ${data.results.length} products`);
676
722
  return data;
677
723
  };
@@ -718,6 +764,11 @@ const createEvoSearchAPI = ({ fetch: fetchImpl = window.fetch, baseURL, userId,
718
764
  return {
719
765
  autocomplete,
720
766
  trending,
767
+ user: {
768
+ recents,
769
+ deleteRecent,
770
+ clearRecents,
771
+ },
721
772
  search,
722
773
  sendEvent,
723
774
  };
@@ -732,11 +783,20 @@ const init = (params) => {
732
783
  try {
733
784
  (_a = window[registryKey]) !== null && _a !== void 0 ? _a : (window[registryKey] = {});
734
785
  logger.info('EvoSearch SDK: Initializing');
786
+ if (new URLSearchParams(window.location.search).get('evs') === 'preview') {
787
+ try {
788
+ sessionStorage.setItem(previewKeyName, '1');
789
+ logger.info('EvoSearch SDK: Preview mode enabled via ?evs=preview URL param');
790
+ }
791
+ catch {
792
+ error('EvoSearch SDK: Failed to set previewEvoSearch sessionStorage');
793
+ }
794
+ }
735
795
  const { enabled, uiPreferences } = (_c = (_b = params.manifest) === null || _b === void 0 ? void 0 : _b.variables) !== null && _c !== void 0 ? _c : {};
736
796
  const mode = uiPreferences === null || uiPreferences === void 0 ? void 0 : uiPreferences.mode;
737
797
  const hasPreview = hasPreviewKey();
738
798
  const shouldEnable = enabled || hasPreview;
739
- if (!shouldEnable || (mode === 'disabled' && !hasPreview)) {
799
+ if (!shouldEnable) {
740
800
  logger.info('EvoSearch SDK: Mode is disabled, skipping initialization');
741
801
  return;
742
802
  }
@@ -748,6 +808,7 @@ const init = (params) => {
748
808
  userId: params.userId,
749
809
  sessionId: (_d = params.session) === null || _d === void 0 ? void 0 : _d.sessionId,
750
810
  sendTag: params.sendTag,
811
+ uiPreferences,
751
812
  });
752
813
  window[registryKey].api = evoSearchAPI;
753
814
  const uiImplementation = window[registryKey].ui;
package/hooks.cjs.js CHANGED
@@ -635,6 +635,7 @@ const extractHandle = (url, fallback) => {
635
635
  }
636
636
  };
637
637
  const mapAPIProductToSDKProduct = (apiProduct) => {
638
+ var _a;
638
639
  const safeUrl = isHttpURL(apiProduct.url) ? apiProduct.url : '';
639
640
  const safeImg = isHttpURL(apiProduct.img) ? apiProduct.img : '';
640
641
  const handle = extractHandle(safeUrl, apiProduct.id);
@@ -652,7 +653,7 @@ const mapAPIProductToSDKProduct = (apiProduct) => {
652
653
  {
653
654
  price: apiProduct.price,
654
655
  comparedAtPrice: apiProduct.compareAtPrice || null,
655
- currencyCode: 'USD', // TODO: Get currency from API or shop config
656
+ currencyCode: (_a = apiProduct.currencyCode) !== null && _a !== void 0 ? _a : '',
656
657
  },
657
658
  ],
658
659
  };