@askdialog/dialog-sdk 2.11.0 → 2.13.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 (48) hide show
  1. package/README.md +31 -20
  2. package/dist/Dialog.d.ts +16 -22
  3. package/dist/Dialog.d.ts.map +1 -1
  4. package/dist/Dialog.js +44 -56
  5. package/dist/__tests__/dialogChangeCartQuantity.spec.d.ts +2 -0
  6. package/dist/__tests__/dialogChangeCartQuantity.spec.d.ts.map +1 -0
  7. package/dist/__tests__/dialogChangeCartQuantity.spec.js +46 -0
  8. package/dist/__tests__/dialogSearchAnalytics.spec.js +2 -4
  9. package/dist/__tests__/publicTypes.spec.js +3 -0
  10. package/dist/__tests__/searchController.spec.js +46 -20
  11. package/dist/__tests__/searchControllerAttribution.spec.js +8 -6
  12. package/dist/__tests__/searchImpressions.spec.js +2 -3
  13. package/dist/__tests__/searchService.spec.js +13 -11
  14. package/dist/searchController.d.ts +1 -1
  15. package/dist/searchController.d.ts.map +1 -1
  16. package/dist/searchController.js +41 -64
  17. package/dist/services/search.d.ts +3 -5
  18. package/dist/services/search.d.ts.map +1 -1
  19. package/dist/services/search.js +11 -6
  20. package/dist/types/constructor.d.ts +9 -3
  21. package/dist/types/constructor.d.ts.map +1 -1
  22. package/dist/types/events.d.ts +5 -0
  23. package/dist/types/events.d.ts.map +1 -1
  24. package/dist/types/search.d.ts +14 -14
  25. package/dist/types/search.d.ts.map +1 -1
  26. package/dist/types/search.js +1 -1
  27. package/dist/types/searchAnalytics.d.ts +12 -6
  28. package/dist/types/searchAnalytics.d.ts.map +1 -1
  29. package/dist/types/searchAnalytics.js +1 -4
  30. package/dist/types/searchController.d.ts +22 -51
  31. package/dist/types/searchController.d.ts.map +1 -1
  32. package/dist/types/searchController.js +1 -1
  33. package/dist/utils/localization.d.ts +0 -1
  34. package/dist/utils/localization.d.ts.map +1 -1
  35. package/dist/utils/localization.js +0 -11
  36. package/dist/utils/searchControllerAnalytics.d.ts +1 -4
  37. package/dist/utils/searchControllerAnalytics.d.ts.map +1 -1
  38. package/dist/utils/searchControllerAnalytics.js +5 -8
  39. package/dist/utils/searchImpressions.d.ts +4 -4
  40. package/dist/utils/searchImpressions.d.ts.map +1 -1
  41. package/dist/utils/searchImpressions.js +4 -11
  42. package/dist/utils/searchRequests.d.ts +17 -0
  43. package/dist/utils/searchRequests.d.ts.map +1 -0
  44. package/dist/utils/searchRequests.js +36 -0
  45. package/package.json +1 -1
  46. package/dist/utils/searchSections.d.ts +0 -5
  47. package/dist/utils/searchSections.d.ts.map +0 -1
  48. 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
@@ -2,7 +2,7 @@ import { DialogConstructor } from "./types/constructor";
2
2
  import { Theme } from "./types/theme";
3
3
  import { DetailedLocaleInfo } from "./utils/localization";
4
4
  import { Suggestion } from "./types/suggestion";
5
- import { AddToCartInput, GenericQuestionPayload, LegacyCheckoutParams, OpenAssistantPayload, ProductQuestionPayload, SubmitCheckoutParams } from "./types/events";
5
+ import { AddToCartInput, ChangeCartQuantityInput, GenericQuestionPayload, LegacyCheckoutParams, OpenAssistantPayload, ProductQuestionPayload, SubmitCheckoutParams } from "./types/events";
6
6
  import { SimplifiedProduct } from "./types/product";
7
7
  import { SelectSearchResultParams, ViewSearchResultsParams } from "./types/searchAnalytics";
8
8
  import { EventsHandler } from "./EventsHandler";
@@ -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;
@@ -59,19 +54,18 @@ export declare class Dialog {
59
54
  dispatchAssistantEvent(event: AssistantEvent): void;
60
55
  getProduct(productId: string, variantId?: string): Promise<SimplifiedProduct>;
61
56
  addToCart(input: AddToCartInput): Promise<void>;
57
+ canChangeCartQuantity(): boolean;
58
+ changeCartQuantity(input: ChangeCartQuantityInput): Promise<void>;
59
+ private _isCartWriteDisabled;
62
60
  private _getCallbacksOrThrow;
63
61
  registerAddToCartEvent(input: AddToCartInput): void;
64
62
  registerSubmitCheckoutEvent(params: SubmitCheckoutParams | LegacyCheckoutParams): void;
65
63
  /**
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.
64
+ * Emit impressions batched by `createSearchImpressionTracker`.
65
+ * Use `items: []` for an empty result set.
69
66
  */
70
67
  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
- */
68
+ /** Emit a selection after its impression, including middle-clicks and modified clicks. */
75
69
  trackSelectSearchResult(params: SelectSearchResultParams): void;
76
70
  private _loadAssistant;
77
71
  }
@@ -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":"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"}
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,27 +154,37 @@ 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.
171
- if (this._disableAddToCart) {
172
- console.warn("Dialog: addToCart is disabled on this instance (disableAddToCart); ignoring the call.");
159
+ // Disabled instances must neither modify the cart nor emit analytics.
160
+ if (this._isCartWriteDisabled("addToCart"))
173
161
  return;
174
- }
175
162
  await this._getCallbacksOrThrow("addToCart").addToCart(input);
176
163
  this.registerAddToCartEvent(input);
177
164
  return;
178
165
  }
179
- // Callbacks are optional at construction; the two commerce methods assert
180
- // theirs at call time with an integration-facing configuration error.
166
+ canChangeCartQuantity() {
167
+ return (!this._disableAddToCart &&
168
+ this._callbacks?.changeCartQuantity !== undefined);
169
+ }
170
+ async changeCartQuantity(input) {
171
+ if (this._isCartWriteDisabled("changeCartQuantity"))
172
+ return;
173
+ await this._getCallbacksOrThrow("changeCartQuantity").changeCartQuantity(input);
174
+ }
175
+ _isCartWriteDisabled(operation) {
176
+ if (!this._disableAddToCart)
177
+ return false;
178
+ console.warn(`Dialog: ${operation} is disabled on this instance (disableAddToCart); ignoring the call.`);
179
+ return true;
180
+ }
181
+ // Validate optional commerce callbacks when invoked.
181
182
  _getCallbacksOrThrow(name) {
182
- if (this._callbacks?.[name] === undefined) {
183
+ const callbacks = this._callbacks;
184
+ if (callbacks?.[name] === undefined) {
183
185
  throw new Error(`Dialog: \`callbacks.${name}\` was not provided to the constructor; ${name}() is unavailable on this instance.`);
184
186
  }
185
- return this._callbacks;
187
+ return callbacks;
186
188
  }
187
189
  registerAddToCartEvent(input) {
188
190
  this._eventsHandler.emitExternalEvent(DialogEvents.TRACK_ADD_TO_CART, {
@@ -190,13 +192,8 @@ export class Dialog {
190
192
  ...input,
191
193
  });
192
194
  }
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.
195
+ // Call once per completed order; `orderValue` supplies the revenue total.
196
+ // The deprecated per-item payload remains accepted but has no order total.
200
197
  registerSubmitCheckoutEvent(params) {
201
198
  if ("orderValue" in params) {
202
199
  this._eventsHandler.emitExternalEvent(DialogEvents.TRACK_SUBMIT_CHECKOUT, {
@@ -218,9 +215,8 @@ export class Dialog {
218
215
  });
219
216
  }
220
217
  /**
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.
218
+ * Emit impressions batched by `createSearchImpressionTracker`.
219
+ * Use `items: []` for an empty result set.
224
220
  */
225
221
  trackViewSearchResults(params) {
226
222
  this._eventsHandler.emitExternalEvent(DialogEvents.TRACK_VIEW_SEARCH_RESULTS, {
@@ -228,10 +224,7 @@ export class Dialog {
228
224
  ...params,
229
225
  });
230
226
  }
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
- */
227
+ /** Emit a selection after its impression, including middle-clicks and modified clicks. */
235
228
  trackSelectSearchResult(params) {
236
229
  this._eventsHandler.emitExternalEvent(DialogEvents.TRACK_SELECT_SEARCH_RESULT, {
237
230
  userId: this._userId,
@@ -252,7 +245,7 @@ export class Dialog {
252
245
  div.dataset.countryCode = localeInfo.countryCode;
253
246
  div.dataset.language = localeInfo.language;
254
247
  if (this._disableAddToCart) {
255
- // Read by the assistant runtime to hide the add-to-cart CTA.
248
+ // Hide the add-to-cart button in the assistant.
256
249
  div.dataset.disableAddToCart = "true";
257
250
  }
258
251
  document.body.appendChild(div);
@@ -262,12 +255,7 @@ export class Dialog {
262
255
  setTimeout(() => {
263
256
  const script = document.createElement("script");
264
257
  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.
258
+ // Set the bypass attribute before src: OneTrust intercepts the src setter.
271
259
  script.setAttribute("data-ot-ignore", "");
272
260
  }
273
261
  script.defer = true;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=dialogChangeCartQuantity.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialogChangeCartQuantity.spec.d.ts","sourceRoot":"","sources":["../../src/__tests__/dialogChangeCartQuantity.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,46 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { Dialog } from "../Dialog";
3
+ const buildDialog = (changeCartQuantity, disableAddToCart = false) => {
4
+ const dialog = Object.create(Dialog.prototype);
5
+ Object.assign(dialog, {
6
+ _callbacks: {
7
+ addToCart: vi.fn(),
8
+ getProduct: vi.fn(),
9
+ ...(changeCartQuantity === undefined ? {} : { changeCartQuantity }),
10
+ },
11
+ _disableAddToCart: disableAddToCart,
12
+ });
13
+ return dialog;
14
+ };
15
+ const input = {
16
+ productId: "gid://shopify/Product/42",
17
+ variantId: "gid://shopify/ProductVariant/4242",
18
+ quantity: 2,
19
+ };
20
+ afterEach(() => {
21
+ vi.clearAllMocks();
22
+ });
23
+ describe("Dialog.changeCartQuantity", () => {
24
+ it("forwards the input to the merchant callback", async () => {
25
+ const changeCartQuantity = vi.fn().mockResolvedValue(undefined);
26
+ const dialog = buildDialog(changeCartQuantity);
27
+ await dialog.changeCartQuantity(input);
28
+ expect(changeCartQuantity).toHaveBeenCalledWith(input);
29
+ expect(dialog.canChangeCartQuantity()).toBe(true);
30
+ });
31
+ it("throws an explicit configuration error without the callback", async () => {
32
+ const dialog = buildDialog(undefined);
33
+ await expect(dialog.changeCartQuantity(input)).rejects.toThrowError(/callbacks\.changeCartQuantity/);
34
+ expect(dialog.canChangeCartQuantity()).toBe(false);
35
+ });
36
+ it("is a no-op and reports no capability when disableAddToCart is set", async () => {
37
+ const changeCartQuantity = vi.fn().mockResolvedValue(undefined);
38
+ const dialog = buildDialog(changeCartQuantity, true);
39
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
40
+ await expect(dialog.changeCartQuantity(input)).resolves.toBeUndefined();
41
+ expect(changeCartQuantity).not.toHaveBeenCalled();
42
+ expect(dialog.canChangeCartQuantity()).toBe(false);
43
+ expect(warn).toHaveBeenCalledOnce();
44
+ warn.mockRestore();
45
+ });
46
+ });
@@ -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,9 @@ describe("public API types", () => {
8
8
  expectTypeOf().toMatchTypeOf();
9
9
  // Invalid callback values must be rejected.
10
10
  expectTypeOf().not.toMatchTypeOf();
11
+ expectTypeOf().toMatchTypeOf();
12
+ expectTypeOf().parameters.toMatchTypeOf();
13
+ expectTypeOf().toEqualTypeOf();
11
14
  expectTypeOf().parameters.toMatchTypeOf();
12
15
  expectTypeOf().toMatchTypeOf();
13
16
  expectTypeOf().toMatchTypeOf();