@askdialog/dialog-sdk 2.11.0 → 2.12.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.
Files changed (43) hide show
  1. package/README.md +31 -20
  2. package/dist/Dialog.d.ts +12 -21
  3. package/dist/Dialog.d.ts.map +1 -1
  4. package/dist/Dialog.js +25 -51
  5. package/dist/__tests__/dialogSearchAnalytics.spec.js +2 -4
  6. package/dist/__tests__/publicTypes.spec.js +1 -0
  7. package/dist/__tests__/searchController.spec.js +46 -20
  8. package/dist/__tests__/searchControllerAttribution.spec.js +8 -6
  9. package/dist/__tests__/searchImpressions.spec.js +2 -3
  10. package/dist/__tests__/searchService.spec.js +13 -11
  11. package/dist/searchController.d.ts +1 -1
  12. package/dist/searchController.d.ts.map +1 -1
  13. package/dist/searchController.js +41 -64
  14. package/dist/services/search.d.ts +3 -5
  15. package/dist/services/search.d.ts.map +1 -1
  16. package/dist/services/search.js +11 -6
  17. package/dist/types/constructor.d.ts +3 -0
  18. package/dist/types/constructor.d.ts.map +1 -1
  19. package/dist/types/search.d.ts +14 -14
  20. package/dist/types/search.d.ts.map +1 -1
  21. package/dist/types/search.js +1 -1
  22. package/dist/types/searchAnalytics.d.ts +12 -6
  23. package/dist/types/searchAnalytics.d.ts.map +1 -1
  24. package/dist/types/searchAnalytics.js +1 -4
  25. package/dist/types/searchController.d.ts +22 -51
  26. package/dist/types/searchController.d.ts.map +1 -1
  27. package/dist/types/searchController.js +1 -1
  28. package/dist/utils/localization.d.ts +0 -1
  29. package/dist/utils/localization.d.ts.map +1 -1
  30. package/dist/utils/localization.js +0 -11
  31. package/dist/utils/searchControllerAnalytics.d.ts +1 -4
  32. package/dist/utils/searchControllerAnalytics.d.ts.map +1 -1
  33. package/dist/utils/searchControllerAnalytics.js +5 -8
  34. package/dist/utils/searchImpressions.d.ts +4 -4
  35. package/dist/utils/searchImpressions.d.ts.map +1 -1
  36. package/dist/utils/searchImpressions.js +4 -11
  37. package/dist/utils/searchRequests.d.ts +17 -0
  38. package/dist/utils/searchRequests.d.ts.map +1 -0
  39. package/dist/utils/searchRequests.js +36 -0
  40. package/package.json +1 -1
  41. package/dist/utils/searchSections.d.ts +0 -5
  42. package/dist/utils/searchSections.d.ts.map +0 -1
  43. package/dist/utils/searchSections.js +0 -21
package/README.md CHANGED
@@ -51,7 +51,8 @@ import { Dialog } from '@askdialog/dialog-sdk';
51
51
 
52
52
  const client = new Dialog({
53
53
  apiKey: 'YOUR_API_KEY', // required
54
- locale: 'TARGETED_LOCALE', // required
54
+ locale: 'fr-FR', currency: 'EUR', // ISO 639-1 language
55
+ currency: 'EUR', // required ISO 4217 currency
55
56
  countryCode: 'FR', // optional, ISO 3166 alpha-2
56
57
  callbacks: {
57
58
  addToCart: async ({
@@ -100,7 +101,7 @@ Declare it at construction on product pages:
100
101
  ```ts
101
102
  new Dialog({
102
103
  apiKey: 'YOUR_API_KEY',
103
- locale: 'fr',
104
+ locale: 'fr-FR', currency: 'EUR',
104
105
  product: { id: 'PRODUCT_ID', variantId: 'VARIANT_ID' }, // variantId optional
105
106
  });
106
107
  ```
@@ -130,7 +131,7 @@ Some sessions must hide purchasing actions — for example a B2B storefront that
130
131
  ```ts
131
132
  new Dialog({
132
133
  apiKey: 'YOUR_API_KEY',
133
- locale: 'fr',
134
+ locale: 'fr-FR', currency: 'EUR',
134
135
  disableAddToCart: true, // hide the add-to-cart CTA for this session
135
136
  });
136
137
  ```
@@ -219,18 +220,20 @@ Example of expected result:
219
220
 
220
221
  - Search products
221
222
 
222
- `client.search()` performs a typed, Algolia-shaped search through Dialog's public API, with no framework and no commerce callbacks required. Each entry targets an index named `<index>_<locale>` (index `products | collections | articles | pages`, locale ISO 639-1) build the name with `searchIndexName(index, locale)`, which reduces any BCP-47 tag to its bare language.
223
+ `client.search()` sends a multi-index request to the public search API. Build names with `searchIndexName(index, language, currency)`: `<index>_<lang>_<currency>`, e.g. `products_fr_eur`. Supported indices: `products`, `collections`, `articles`, `pages`. Language must be a lowercase ISO 639-1 code (`fr`, `en`). Regional locales such as `fr-FR` are rejected. The ISO 4217 currency is lowercased in the index name.
224
+
225
+ Currency is required and independent of language: `fr` with `USD` produces `products_fr_usd`. Names without a currency suffix return 404.
223
226
 
224
227
  ```typescript
225
228
  import { Dialog, DialogSearchError, searchIndexName } from '@askdialog/dialog-sdk';
226
229
  import type { SearchResponse } from '@askdialog/dialog-sdk';
227
230
 
228
- const client = new Dialog({ apiKey: 'YOUR_API_KEY', locale: 'fr' });
231
+ const client = new Dialog({ apiKey: 'YOUR_API_KEY', locale: 'fr-FR', currency: 'EUR' });
229
232
 
230
233
  const response: SearchResponse = await client.search({
231
234
  requests: [
232
235
  {
233
- indexName: searchIndexName('products', client.locale), // "products_fr"
236
+ indexName: searchIndexName('products', 'fr', client.currency), // "products_fr_eur"
234
237
  query: 'shampoo',
235
238
  page: 0, // optional, zero-indexed (default 0)
236
239
  hitsPerPage: 20, // optional, 1-100 (default 20)
@@ -248,29 +251,32 @@ With the IIFE bundle the results are plain runtime JSON (same shape, no types):
248
251
  ```html
249
252
  <script src="https://d2m6yt8rnm4dos.cloudfront.net/dialog-sdk.X.Y.Z.min.js"></script>
250
253
  <script>
251
- const client = new window.DialogSDK.Dialog({ apiKey: 'YOUR_API_KEY', locale: 'fr' });
254
+ const client = new window.DialogSDK.Dialog({ apiKey: 'YOUR_API_KEY', locale: 'fr-FR', currency: 'EUR' });
252
255
  client
253
- .search({ requests: [{ indexName: 'products_fr', query: 'shampoo' }] })
256
+ .search({ requests: [{ indexName: 'products_fr_eur', query: 'shampoo' }] })
254
257
  .then((response) => console.log(response.results[0].hits));
255
258
  </script>
256
259
  ```
257
260
 
258
261
  A non-2xx answer rejects with `DialogSearchError` — stable `name`, HTTP `status` and `message` (e.g. `404 Index products_xx does not exist`, `400 Unknown parameter: foo`). Aborting rejects with the native `AbortError`, and network failures keep their native errors.
259
262
 
260
- `client.search()` itself is stateless: no debounce, no cache, no automatic cancellation of previous searches. For search-as-you-type, use the search controller below instead of hand-rolling those.
263
+ `client.search()` sends requests unchanged, without debounce, caching or automatic cancellation. Use `createSearchController()` for interactive search.
261
264
 
262
265
  - Search controller
263
266
 
264
- `createSearchController()` wraps the stateless transport with the stateful behavior every search UI needs — debounce (immediate on explicit submission), cancellation of the in-flight request, stale-response protection (a late response never replaces newer results, even if the transport ignores the abort), pagination that resets on a new query, `idle` / `loading` / `success` / `empty` / `error` states, retry, and the attribution events (`view_search_results` viewport impressions, `select_search_result` clicks). It searches the products index (`products_<locale>`) and exposes the products result entry as `state.response`; optional `sections` add other indices (`collections`, …) to the same request, exposed under `state.sections`. It has no framework or rendering dependency: raw JavaScript, React, Vue and Shopify integrations are rendering-and-routing adapters around it.
267
+ `createSearchController()` handles debounce, cancellation, stale responses, pagination, retries and search analytics. New queries reset pagination. State is `idle`, `loading`, `success`, `empty` or `error`.
268
+
269
+ Products are available in `state.response`. Optional `sections` query additional indexes in the same request and expose results in `state.sections`.
265
270
 
266
271
  ```typescript
267
272
  import { createSearchController, Dialog, SearchStatus } from '@askdialog/dialog-sdk';
268
273
 
269
- const client = new Dialog({ apiKey: 'YOUR_API_KEY', locale: 'fr' });
274
+ const client = new Dialog({ apiKey: 'YOUR_API_KEY', locale: 'fr-FR', currency: 'EUR' });
270
275
 
271
276
  const controller = createSearchController({
272
277
  search: (request, options) => client.search(request, options),
273
- locale: client.locale, // names the searched index ("products_fr")
278
+ language: 'fr',
279
+ currency: client.currency,
274
280
  analytics: {
275
281
  surface: 'search_page', // where results are displayed
276
282
  trackViewSearchResults: (params) => client.trackViewSearchResults(params),
@@ -283,7 +289,7 @@ const controller = createSearchController({
283
289
  });
284
290
 
285
291
  const unsubscribe = controller.subscribe((state) => {
286
- // state: { status, query, page, response?, error? }
292
+ // state: { status, query, page, response?, sections?, error? }
287
293
  if (state.status === SearchStatus.SUCCESS) {
288
294
  renderCards(state.response.hits).forEach((element, index) => {
289
295
  controller.observeResult(element, index); // viewport impression
@@ -296,16 +302,20 @@ input.oninput = () => controller.setQuery(input.value); // debounced
296
302
  form.onsubmit = () => controller.submit(input.value); // immediate
297
303
  nextButton.onclick = () => controller.setPage(controller.getState().page + 1);
298
304
  retryButton.onclick = () => controller.retry();
299
- // On teardown (SPA unmount): cancel in-flight work and detach observers.
305
+ // Cancel requests and remove observers on unmount.
300
306
  controller.dispose();
301
307
  ```
302
308
 
303
- The adapter contract for a framework binding (React, Vue, Shopify):
309
+ The client uses a BCP-47 `locale` such as `fr-FR` for assistant localization. Search controllers and React/Vue hooks require explicit `language` (ISO 639-1) and `currency` (ISO 4217), independently of the client locale. Pass `client.currency` to reuse its configured currency.
310
+
311
+ For Shopify, use `window.Shopify.currency.active` as the currency. Controller options are fixed at creation; recreate the controller to change language or currency.
312
+
313
+ Framework integrations:
304
314
 
305
- - **Rendering** — subscribe to the controller (`subscribe`/`getState` fit React's `useSyncExternalStore` and a Vue `shallowRef` updated by the listener) and render the five states; never re-implement debounce, `AbortController` or race protection locally.
306
- - **Attribution** — call `observeResult(element, index)` for every rendered result and `selectResult(index)` on every result click (including middle-click/cmd+click). Do not `preventDefault` a same-tab navigation: attribution is recorded first and the events survive it.
307
- - **Routing** — platform navigation and URL synchronization (query params, history) stay in the adapter: pass `navigate` for router-driven platforms, or let plain `<a href>` links navigate natively.
308
- - **Lifecycle** — create one controller per search surface and `dispose()` it on unmount.
315
+ - Subscribe to state changes and render results.
316
+ - Call `observeResult(element, index)` for each result and `selectResult(index)` on selection. Use `{ navigate: false }` for middle-clicks and modified clicks. Call `preventDefault()` only when `selectResult` returns true.
317
+ - Pass `navigate` for router navigation. The integration owns URL synchronization.
318
+ - Create one controller per search surface and call `dispose()` on unmount.
309
319
 
310
320
  The raw JavaScript reference adapter lives in [`packages/search-example`](../search-example).
311
321
 
@@ -531,7 +541,8 @@ interface AssistantEvent {
531
541
  payload: {
532
542
  // Common fields (included in all events)
533
543
  date: string; // ISO timestamp
534
- locale: string; // Current locale
544
+ locale: string; // BCP-47 locale
545
+ currency: string; // Current currency
535
546
  url: string; // Current page URL
536
547
  userId?: string; // User ID if available
537
548
 
package/dist/Dialog.d.ts CHANGED
@@ -12,6 +12,7 @@ export declare class Dialog {
12
12
  static readonly VERSION: string;
13
13
  private _apiKey;
14
14
  private _locale;
15
+ private _currency;
15
16
  private _countryCode?;
16
17
  private _callbacks?;
17
18
  private _theme;
@@ -20,37 +21,31 @@ export declare class Dialog {
20
21
  private _ignoreOneTrustAutoBlock;
21
22
  private _disableAddToCart;
22
23
  private _currentProduct?;
23
- constructor({ apiKey, locale, countryCode, callbacks, theme, userId, ignoreOneTrustAutoBlock, disableAddToCart, product, }: DialogConstructor);
24
+ constructor({ apiKey, locale, currency, countryCode, callbacks, theme, userId, ignoreOneTrustAutoBlock, disableAddToCart, product, }: DialogConstructor);
24
25
  get apiKey(): string;
25
26
  get theme(): Theme;
26
27
  get userId(): string;
27
28
  get locale(): string;
29
+ get currency(): string;
28
30
  get eventsHandler(): EventsHandler;
29
31
  getLocalizationInformations(): DetailedLocaleInfo | null;
30
32
  private _createOrRetrieveUserId;
31
33
  getSuggestions(productId: string): Promise<Suggestion>;
32
34
  /**
33
- * Typed multi-index search through the Nest public endpoint (Algolia-shaped,
34
- * DAT-412). The locale travels in each entry's `indexName` (`products_fr`) —
35
- * build it with `searchIndexName(index, locale)`. Stateless: one request per
36
- * call, cancellation belongs to the caller via `options.signal` (previous
37
- * searches are never cancelled automatically). Rejects with
38
- * `DialogSearchError` on a non-2xx answer, and with the native AbortError /
39
- * network error otherwise.
35
+ * Send one search request with names built by `searchIndexName`.
36
+ * Cancel through `options.signal`. HTTP errors reject with `DialogSearchError`;
37
+ * network and cancellation errors propagate unchanged.
40
38
  */
41
39
  search(request: SearchRequest, options?: SearchOptions): Promise<SearchResponse>;
42
40
  openAssistant(params: OpenAssistantPayload): void;
43
41
  closeAssistant(): void;
44
42
  /**
45
- * Declare the product of the current page. The assistant reads it as the
46
- * conversation's product context whenever a question carries no product of
47
- * its own (floating bookmark, resume surface, free-text input). Call it on
48
- * every PDP navigation on single-page storefronts; ids must match the
49
- * Dialog product feed.
43
+ * Set the default product context for assistant questions.
44
+ * Call on each product-page navigation; IDs must match the Dialog product feed.
50
45
  */
51
46
  setCurrentProduct(productId: string, variantId?: string): void;
52
47
  private static _isValidProductId;
53
- /** Declare that the current page is not a product page. */
48
+ /** Clear the product context on non-product pages. */
54
49
  clearCurrentProduct(): void;
55
50
  private _applyCurrentProductDataset;
56
51
  sendProductMessage(params: ProductQuestionPayload): void;
@@ -63,15 +58,11 @@ export declare class Dialog {
63
58
  registerAddToCartEvent(input: AddToCartInput): void;
64
59
  registerSubmitCheckoutEvent(params: SubmitCheckoutParams | LegacyCheckoutParams): void;
65
60
  /**
66
- * Emit a batch of viewport impressions. The batching/dedup semantics live
67
- * in `createSearchImpressionTracker` wire its `emit` here, or call
68
- * directly with `items: []` for a rendered no-results state.
61
+ * Emit impressions batched by `createSearchImpressionTracker`.
62
+ * Use `items: []` for an empty result set.
69
63
  */
70
64
  trackViewSearchResults(params: ViewSearchResultsParams): void;
71
- /**
72
- * Emit on a result click — including auxclick / cmd+click, not only before
73
- * a same-tab navigation. Force the clicked item's impression first.
74
- */
65
+ /** Emit a selection after its impression, including middle-clicks and modified clicks. */
75
66
  trackSelectSearchResult(params: SelectSearchResultParams): void;
76
67
  private _loadAssistant;
77
68
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Dialog.d.ts","sourceRoot":"","sources":["../src/Dialog.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Dialog.d.ts","sourceRoot":"","sources":["../src/Dialog.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,iBAAiB,EAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,OAAO,EACL,kBAAkB,EAEnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EACL,cAAc,EAEd,sBAAsB,EACtB,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,EACtB,oBAAoB,EACrB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,qBAAa,MAAM;IACjB,gBAAuB,OAAO,SAAuB;IAErD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,YAAY,CAAC,CAAS;IAE9B,OAAO,CAAC,UAAU,CAAC,CAAkB;IACrC,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,wBAAwB,CAAU;IAC1C,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,eAAe,CAAC,CAAiB;gBAE7B,EACV,MAAM,EACN,MAAM,EACN,QAAQ,EACR,WAAW,EACX,SAAS,EACT,KAAK,EACL,MAAM,EACN,uBAAuB,EACvB,gBAAgB,EAChB,OAAO,GACR,EAAE,iBAAiB;IAwBpB,IAAW,MAAM,IAAI,MAAM,CAE1B;IACD,IAAW,KAAK,IAAI,KAAK,CAExB;IACD,IAAW,MAAM,IAAI,MAAM,CAE1B;IACD,IAAW,MAAM,IAAI,MAAM,CAE1B;IACD,IAAW,QAAQ,IAAI,MAAM,CAE5B;IACD,IAAW,aAAa,IAAI,aAAa,CAExC;IAEM,2BAA2B,IAAI,kBAAkB,GAAG,IAAI;IAI/D,OAAO,CAAC,uBAAuB;IAkBlB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAInE;;;;OAIG;IACI,MAAM,CACX,OAAO,EAAE,aAAa,EACtB,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,cAAc,CAAC;IAKnB,aAAa,CAAC,MAAM,EAAE,oBAAoB,GAAG,IAAI;IAKjD,cAAc,IAAI,IAAI;IAI7B;;;OAGG;IACI,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAYrE,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAIhC,sDAAsD;IAC/C,mBAAmB,IAAI,IAAI;IAMlC,OAAO,CAAC,2BAA2B;IA2B5B,kBAAkB,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI;IAIxD,kBAAkB,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI;IAOxD,gBAAgB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,IAAI;IAIjE,sBAAsB,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IAInD,UAAU,CACf,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,CAAC;IAQhB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB5D,OAAO,CAAC,oBAAoB;IAUrB,sBAAsB,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IASnD,2BAA2B,CAChC,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,GAClD,IAAI;IA0BP;;;OAGG;IACI,sBAAsB,CAAC,MAAM,EAAE,uBAAuB,GAAG,IAAI;IAUpE,0FAA0F;IACnF,uBAAuB,CAAC,MAAM,EAAE,wBAAwB,GAAG,IAAI;IAUtE,OAAO,CAAC,cAAc;CAsCvB"}
package/dist/Dialog.js CHANGED
@@ -14,6 +14,7 @@ export class Dialog {
14
14
  static VERSION = packageJson.version;
15
15
  _apiKey;
16
16
  _locale;
17
+ _currency;
17
18
  _countryCode;
18
19
  _callbacks;
19
20
  _theme;
@@ -22,9 +23,10 @@ export class Dialog {
22
23
  _ignoreOneTrustAutoBlock;
23
24
  _disableAddToCart;
24
25
  _currentProduct;
25
- constructor({ apiKey, locale, countryCode, callbacks, theme, userId, ignoreOneTrustAutoBlock, disableAddToCart, product, }) {
26
+ constructor({ apiKey, locale, currency, countryCode, callbacks, theme, userId, ignoreOneTrustAutoBlock, disableAddToCart, product, }) {
26
27
  this._apiKey = apiKey;
27
28
  this._locale = locale;
29
+ this._currency = currency;
28
30
  this._countryCode = countryCode;
29
31
  this._callbacks = callbacks;
30
32
  this._ignoreOneTrustAutoBlock = ignoreOneTrustAutoBlock ?? false;
@@ -54,6 +56,9 @@ export class Dialog {
54
56
  get locale() {
55
57
  return this._locale;
56
58
  }
59
+ get currency() {
60
+ return this._currency;
61
+ }
57
62
  get eventsHandler() {
58
63
  return this._eventsHandler;
59
64
  }
@@ -77,13 +82,9 @@ export class Dialog {
77
82
  return loadSuggestions(this._apiKey, this._locale, productId);
78
83
  }
79
84
  /**
80
- * Typed multi-index search through the Nest public endpoint (Algolia-shaped,
81
- * DAT-412). The locale travels in each entry's `indexName` (`products_fr`) —
82
- * build it with `searchIndexName(index, locale)`. Stateless: one request per
83
- * call, cancellation belongs to the caller via `options.signal` (previous
84
- * searches are never cancelled automatically). Rejects with
85
- * `DialogSearchError` on a non-2xx answer, and with the native AbortError /
86
- * network error otherwise.
85
+ * Send one search request with names built by `searchIndexName`.
86
+ * Cancel through `options.signal`. HTTP errors reject with `DialogSearchError`;
87
+ * network and cancellation errors propagate unchanged.
87
88
  */
88
89
  search(request, options) {
89
90
  return searchLexical(this._apiKey, request, options);
@@ -97,11 +98,8 @@ export class Dialog {
97
98
  this._eventsHandler.emitExternalEvent(DialogEvents.CLOSE_ASSISTANT);
98
99
  }
99
100
  /**
100
- * Declare the product of the current page. The assistant reads it as the
101
- * conversation's product context whenever a question carries no product of
102
- * its own (floating bookmark, resume surface, free-text input). Call it on
103
- * every PDP navigation on single-page storefronts; ids must match the
104
- * Dialog product feed.
101
+ * Set the default product context for assistant questions.
102
+ * Call on each product-page navigation; IDs must match the Dialog product feed.
105
103
  */
106
104
  setCurrentProduct(productId, variantId) {
107
105
  if (!Dialog._isValidProductId(productId)) {
@@ -114,23 +112,17 @@ export class Dialog {
114
112
  static _isValidProductId(productId) {
115
113
  return typeof productId === "string" && productId.trim() !== "";
116
114
  }
117
- /** Declare that the current page is not a product page. */
115
+ /** Clear the product context on non-product pages. */
118
116
  clearCurrentProduct() {
119
117
  this._currentProduct = undefined;
120
118
  this._applyCurrentProductDataset();
121
119
  }
122
- // The dataset on the assistant mount node is the SDK⇄runtime contract for
123
- // the page product: the runtime observes data-product-id / data-variant-id
124
- // and needs no event plumbing, so SDK and runtime versions can drift.
120
+ // The runtime reads product context from the mount node dataset.
125
121
  _applyCurrentProductDataset() {
126
- // Resolved via getElementById on purpose, NOT an instance reference: the
127
- // assistant runtime reads the dataset the same way, so on a page where
128
- // another integration already rendered #dialog-shopify-ai (duplicate-id
129
- // case) writer and reader must land on the same — first — node.
122
+ // Match the runtime lookup: duplicate IDs must resolve to the same first node.
130
123
  const mountNode = document.getElementById("dialog-shopify-ai");
131
124
  if (mountNode === null) {
132
- // Assistant mount node absent (locale error at load, or the host page
133
- // removed it) — the declaration is kept but cannot reach the runtime.
125
+ // Keep the context until a mount node is available.
134
126
  console.warn("Dialog: assistant mount node not found; the current product declaration has no effect.");
135
127
  return;
136
128
  }
@@ -162,12 +154,9 @@ export class Dialog {
162
154
  getProduct(productId, variantId) {
163
155
  return this._getCallbacksOrThrow("getProduct").getProduct(productId, variantId);
164
156
  }
165
- // The full input (including the optional enriched product fields) is
166
- // forwarded to the merchant callback and to the tracking event, so
167
- // integrations can consume the added-product data wherever they hook in.
157
+ // Forward all product fields to the commerce callback and analytics.
168
158
  async addToCart(input) {
169
- // No-op when disabled, so a stale UI that still surfaced the CTA cannot
170
- // add to the cart or emit analytics.
159
+ // Disabled instances must neither modify the cart nor emit analytics.
171
160
  if (this._disableAddToCart) {
172
161
  console.warn("Dialog: addToCart is disabled on this instance (disableAddToCart); ignoring the call.");
173
162
  return;
@@ -176,8 +165,7 @@ export class Dialog {
176
165
  this.registerAddToCartEvent(input);
177
166
  return;
178
167
  }
179
- // Callbacks are optional at construction; the two commerce methods assert
180
- // theirs at call time with an integration-facing configuration error.
168
+ // Validate optional commerce callbacks when invoked.
181
169
  _getCallbacksOrThrow(name) {
182
170
  if (this._callbacks?.[name] === undefined) {
183
171
  throw new Error(`Dialog: \`callbacks.${name}\` was not provided to the constructor; ${name}() is unavailable on this instance.`);
@@ -190,13 +178,8 @@ export class Dialog {
190
178
  ...input,
191
179
  });
192
180
  }
193
- // Order-level: call ONCE per completed order with the order total.
194
- // `orderValue` is what the dashboard's "Revenue generated" reads do not
195
- // call this per line item (that has no total and revenue resolves to 0).
196
- //
197
- // The legacy per-line signature (`{ productId, quantity, price }`) is still
198
- // accepted for backward compatibility so existing installs keep working
199
- // after an upgrade, but it is deprecated: it carries no order total.
181
+ // Call once per completed order; `orderValue` supplies the revenue total.
182
+ // The deprecated per-item payload remains accepted but has no order total.
200
183
  registerSubmitCheckoutEvent(params) {
201
184
  if ("orderValue" in params) {
202
185
  this._eventsHandler.emitExternalEvent(DialogEvents.TRACK_SUBMIT_CHECKOUT, {
@@ -218,9 +201,8 @@ export class Dialog {
218
201
  });
219
202
  }
220
203
  /**
221
- * Emit a batch of viewport impressions. The batching/dedup semantics live
222
- * in `createSearchImpressionTracker` wire its `emit` here, or call
223
- * directly with `items: []` for a rendered no-results state.
204
+ * Emit impressions batched by `createSearchImpressionTracker`.
205
+ * Use `items: []` for an empty result set.
224
206
  */
225
207
  trackViewSearchResults(params) {
226
208
  this._eventsHandler.emitExternalEvent(DialogEvents.TRACK_VIEW_SEARCH_RESULTS, {
@@ -228,10 +210,7 @@ export class Dialog {
228
210
  ...params,
229
211
  });
230
212
  }
231
- /**
232
- * Emit on a result click — including auxclick / cmd+click, not only before
233
- * a same-tab navigation. Force the clicked item's impression first.
234
- */
213
+ /** Emit a selection after its impression, including middle-clicks and modified clicks. */
235
214
  trackSelectSearchResult(params) {
236
215
  this._eventsHandler.emitExternalEvent(DialogEvents.TRACK_SELECT_SEARCH_RESULT, {
237
216
  userId: this._userId,
@@ -252,7 +231,7 @@ export class Dialog {
252
231
  div.dataset.countryCode = localeInfo.countryCode;
253
232
  div.dataset.language = localeInfo.language;
254
233
  if (this._disableAddToCart) {
255
- // Read by the assistant runtime to hide the add-to-cart CTA.
234
+ // Hide the add-to-cart button in the assistant.
256
235
  div.dataset.disableAddToCart = "true";
257
236
  }
258
237
  document.body.appendChild(div);
@@ -262,12 +241,7 @@ export class Dialog {
262
241
  setTimeout(() => {
263
242
  const script = document.createElement("script");
264
243
  if (this._ignoreOneTrustAutoBlock) {
265
- // OneTrust auto-blocking also intercepts dynamically injected scripts
266
- // (by domain), so the merchant's data-ot-ignore on their own SDK tag
267
- // cannot cover this one — it has to be set here. OneTrust's trap fires
268
- // synchronously inside the `src` setter, so the attribute must be in
269
- // place BEFORE src is assigned or the script is rewritten to
270
- // type="text/plain" despite carrying the attribute.
244
+ // Set the bypass attribute before src: OneTrust intercepts the src setter.
271
245
  script.setAttribute("data-ot-ignore", "");
272
246
  }
273
247
  script.defer = true;
@@ -2,9 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
2
2
  import { Dialog } from "../Dialog";
3
3
  import { EventsHandler } from "../EventsHandler";
4
4
  import { DIALOG_CUSTOM_EVENT, DialogEvents } from "../types/events";
5
- // The Dialog constructor loads the assistant into the DOM; these tests only
6
- // exercise the search tracking methods, so the instance is built from the
7
- // prototype with just the fields those methods read.
5
+ // Bypass constructor DOM setup; provide only the fields used by analytics.
8
6
  const buildDialog = () => {
9
7
  const eventsHandler = new EventsHandler("fr", "user-1");
10
8
  const emitExternalEvent = vi.fn();
@@ -19,7 +17,7 @@ const buildDialog = () => {
19
17
  };
20
18
  const envelope = {
21
19
  query_id: "query-1",
22
- index: "products_fr",
20
+ index: "products_fr_eur",
23
21
  surface: "search_page",
24
22
  search_type: "lexical",
25
23
  page: 2,
@@ -8,6 +8,7 @@ describe("public API types", () => {
8
8
  expectTypeOf().toMatchTypeOf();
9
9
  // Invalid callback values must be rejected.
10
10
  expectTypeOf().not.toMatchTypeOf();
11
+ expectTypeOf().toEqualTypeOf();
11
12
  expectTypeOf().parameters.toMatchTypeOf();
12
13
  expectTypeOf().toMatchTypeOf();
13
14
  expectTypeOf().toMatchTypeOf();
@@ -1,4 +1,4 @@
1
- /* eslint max-lines: ["error", 460] */
1
+ /* eslint max-lines: ["error", 490] */
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { createSearchController } from "../searchController";
4
4
  import { SearchStatus } from "../types/searchController";
@@ -15,7 +15,7 @@ const DEBOUNCE_MS = 250;
15
15
  const response = (overrides = {}) => ({
16
16
  results: [
17
17
  {
18
- index: "products_fr",
18
+ index: "products_fr_eur",
19
19
  hits: [{ objectID: "p1" }, { objectID: "p2" }],
20
20
  nbHits: 2,
21
21
  page: 0,
@@ -37,7 +37,7 @@ const deferred = () => {
37
37
  });
38
38
  return { promise, resolve, reject };
39
39
  };
40
- // Lets the controller's `await search(...)` continuation run.
40
+ // Flush the search promise continuation.
41
41
  const settle = async () => {
42
42
  await Promise.resolve();
43
43
  await Promise.resolve();
@@ -49,12 +49,13 @@ const states = [];
49
49
  const createController = () => {
50
50
  const controller = createSearchController({
51
51
  search,
52
+ language: "fr",
53
+ currency: "EUR",
52
54
  analytics: {
53
55
  surface: "search_page",
54
56
  trackViewSearchResults,
55
57
  trackSelectSearchResult,
56
58
  },
57
- locale: "fr",
58
59
  });
59
60
  controller.subscribe((state) => states.push(state));
60
61
  return controller;
@@ -93,22 +94,28 @@ describe("createSearchController", () => {
93
94
  });
94
95
  expect(controller.getState().status).toBe(SearchStatus.SUCCESS);
95
96
  });
96
- it("sends one products entry named after the bare-language locale", async () => {
97
+ it("sends one products entry with independent language and currency", async () => {
97
98
  const controller = createSearchController({
98
99
  search,
100
+ language: "fr",
101
+ currency: "USD",
99
102
  analytics: {
100
103
  surface: "search_page",
101
104
  trackViewSearchResults,
102
105
  trackSelectSearchResult,
103
106
  },
104
- locale: "fr-FR",
105
107
  });
106
- search.mockResolvedValue(response({ query: "shoes" }));
108
+ search.mockResolvedValue(response({ query: "shoes", index: "products_fr_usd" }));
107
109
  controller.submit("shoes");
108
110
  await settle();
109
111
  expect(search.mock.calls[0][0]).toEqual({
110
112
  requests: [
111
- { indexName: "products_fr", query: "shoes", page: 0, hitsPerPage: 12 },
113
+ {
114
+ indexName: "products_fr_usd",
115
+ query: "shoes",
116
+ page: 0,
117
+ hitsPerPage: 12,
118
+ },
112
119
  ],
113
120
  });
114
121
  controller.dispose();
@@ -125,7 +132,7 @@ describe("createSearchController", () => {
125
132
  });
126
133
  expect(controller.getState().status).toBe(SearchStatus.SUCCESS);
127
134
  });
128
- it("aborts the previous in-flight request when a new one starts", async () => {
135
+ it("invalidates the previous request as soon as new input is queued", async () => {
129
136
  const controller = createController();
130
137
  const first = deferred();
131
138
  const second = deferred();
@@ -134,8 +141,14 @@ describe("createSearchController", () => {
134
141
  .mockReturnValueOnce(second.promise);
135
142
  controller.submit("shoes");
136
143
  const firstSignal = search.mock.calls[0][1]?.signal;
137
- controller.submit("boots");
144
+ controller.setQuery("boots");
145
+ first.resolve(response({ query: "shoes" }));
146
+ await settle();
138
147
  expect(firstSignal?.aborted).toBe(true);
148
+ expect(search).toHaveBeenCalledTimes(1);
149
+ expect(controller.getState().response).toBeUndefined();
150
+ expect(controller.getState().status).toBe(SearchStatus.LOADING);
151
+ vi.advanceTimersByTime(DEBOUNCE_MS);
139
152
  second.resolve(response({ query: "boots", queryID: "qid-2" }));
140
153
  await settle();
141
154
  expect(controller.getState().response?.query).toBe("boots");
@@ -151,7 +164,7 @@ describe("createSearchController", () => {
151
164
  controller.submit("boots");
152
165
  fresh.resolve(response({ query: "boots", queryID: "qid-2" }));
153
166
  await settle();
154
- // The stale transport ignored the abort and answers after the fresh one.
167
+ // Simulate a late response from a transport that ignores cancellation.
155
168
  stale.resolve(response({ query: "shoes", queryID: "qid-1" }));
156
169
  await settle();
157
170
  expect(controller.getState().status).toBe(SearchStatus.SUCCESS);
@@ -291,42 +304,54 @@ describe("createSearchController", () => {
291
304
  it("requests the sections on their first page with the products and exposes their entries", async () => {
292
305
  const controller = createSearchController({
293
306
  search,
307
+ language: "fr",
308
+ currency: "USD",
294
309
  analytics: {
295
310
  surface: "search_page",
296
311
  trackViewSearchResults,
297
312
  trackSelectSearchResult,
298
313
  },
299
- locale: "fr",
300
314
  sections: [{ index: "collections", hitsPerPage: 5 }],
301
315
  });
302
316
  const collections = {
303
317
  ...response().results[0],
304
- index: "collections_fr",
318
+ index: "collections_fr_usd",
305
319
  hits: [{ objectID: "c1" }],
306
320
  nbHits: 1,
307
321
  };
308
322
  search.mockResolvedValueOnce({
309
- results: [...response().results, collections],
323
+ results: [...response({ index: "products_fr_usd" }).results, collections],
310
324
  });
311
325
  controller.submit("shoes");
312
326
  await settle();
313
327
  expect(search.mock.calls[0][0].requests).toEqual([
314
- { indexName: "products_fr", query: "shoes", page: 0, hitsPerPage: 12 },
315
- { indexName: "collections_fr", query: "shoes", page: 0, hitsPerPage: 5 },
328
+ {
329
+ indexName: "products_fr_usd",
330
+ query: "shoes",
331
+ page: 0,
332
+ hitsPerPage: 12,
333
+ },
334
+ {
335
+ indexName: "collections_fr_usd",
336
+ query: "shoes",
337
+ page: 0,
338
+ hitsPerPage: 5,
339
+ },
316
340
  ]);
317
- expect(controller.getState().response?.index).toBe("products_fr");
341
+ expect(controller.getState().response?.index).toBe("products_fr_usd");
318
342
  expect(controller.getState().sections).toEqual({ collections });
319
343
  });
320
344
  it("resolves a sections function per request and leaves a missing entry out", async () => {
321
345
  let enabled = false;
322
346
  const controller = createSearchController({
323
347
  search,
348
+ language: "fr",
349
+ currency: "EUR",
324
350
  analytics: {
325
351
  surface: "search_page",
326
352
  trackViewSearchResults,
327
353
  trackSelectSearchResult,
328
354
  },
329
- locale: "fr",
330
355
  sections: () => (enabled ? [{ index: "collections" }] : []),
331
356
  });
332
357
  search.mockResolvedValue(response());
@@ -338,7 +363,7 @@ describe("createSearchController", () => {
338
363
  expect(search.mock.calls[0][0].requests).toHaveLength(1);
339
364
  expect(search.mock.calls[1][0].requests).toHaveLength(2);
340
365
  expect(search.mock.calls[1][0].requests[1]).toMatchObject({
341
- indexName: "collections_fr",
366
+ indexName: "collections_fr_eur",
342
367
  hitsPerPage: 12,
343
368
  });
344
369
  expect(controller.getState().sections).toEqual({});
@@ -347,12 +372,13 @@ describe("createSearchController", () => {
347
372
  const failure = new Error("entitlement lookup failed");
348
373
  const controller = createSearchController({
349
374
  search,
375
+ language: "fr",
376
+ currency: "EUR",
350
377
  analytics: {
351
378
  surface: "search_page",
352
379
  trackViewSearchResults,
353
380
  trackSelectSearchResult,
354
381
  },
355
- locale: "fr",
356
382
  sections: () => {
357
383
  throw failure;
358
384
  },