@askdialog/dialog-sdk 2.10.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.
- package/README.md +32 -20
- package/dist/Dialog.d.ts +12 -21
- package/dist/Dialog.d.ts.map +1 -1
- package/dist/Dialog.js +25 -51
- package/dist/__tests__/dialogSearchAnalytics.spec.js +2 -4
- package/dist/__tests__/publicTypes.spec.js +1 -0
- package/dist/__tests__/searchController.spec.js +116 -11
- package/dist/__tests__/searchControllerAttribution.spec.js +8 -6
- package/dist/__tests__/searchImpressions.spec.js +2 -3
- package/dist/__tests__/searchService.spec.js +13 -11
- package/dist/searchController.d.ts +1 -1
- package/dist/searchController.d.ts.map +1 -1
- package/dist/searchController.js +47 -57
- package/dist/services/search.d.ts +3 -5
- package/dist/services/search.d.ts.map +1 -1
- package/dist/services/search.js +11 -6
- package/dist/types/constructor.d.ts +3 -0
- package/dist/types/constructor.d.ts.map +1 -1
- package/dist/types/search.d.ts +14 -14
- package/dist/types/search.d.ts.map +1 -1
- package/dist/types/search.js +1 -1
- package/dist/types/searchAnalytics.d.ts +12 -6
- package/dist/types/searchAnalytics.d.ts.map +1 -1
- package/dist/types/searchAnalytics.js +1 -4
- package/dist/types/searchController.d.ts +29 -52
- package/dist/types/searchController.d.ts.map +1 -1
- package/dist/types/searchController.js +1 -1
- package/dist/utils/localization.d.ts +0 -1
- package/dist/utils/localization.d.ts.map +1 -1
- package/dist/utils/localization.js +0 -11
- package/dist/utils/searchControllerAnalytics.d.ts +1 -4
- package/dist/utils/searchControllerAnalytics.d.ts.map +1 -1
- package/dist/utils/searchControllerAnalytics.js +5 -8
- package/dist/utils/searchImpressions.d.ts +4 -4
- package/dist/utils/searchImpressions.d.ts.map +1 -1
- package/dist/utils/searchImpressions.js +4 -11
- package/dist/utils/searchRequests.d.ts +17 -0
- package/dist/utils/searchRequests.d.ts.map +1 -0
- package/dist/utils/searchRequests.js +36 -0
- package/package.json +1 -1
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: '
|
|
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()`
|
|
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.
|
|
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: '
|
|
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()`
|
|
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()`
|
|
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
|
-
|
|
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),
|
|
@@ -279,10 +285,11 @@ const controller = createSearchController({
|
|
|
279
285
|
navigate: (url) => router.push(url), // optional platform routing adapter
|
|
280
286
|
debounceMs: 250, // optional (default 250)
|
|
281
287
|
hitsPerPage: 12, // optional (default 12)
|
|
288
|
+
sections: [{ index: 'collections', hitsPerPage: 5 }], // optional, first page only; a function is resolved per request
|
|
282
289
|
});
|
|
283
290
|
|
|
284
291
|
const unsubscribe = controller.subscribe((state) => {
|
|
285
|
-
// state: { status, query, page, response?, error? }
|
|
292
|
+
// state: { status, query, page, response?, sections?, error? }
|
|
286
293
|
if (state.status === SearchStatus.SUCCESS) {
|
|
287
294
|
renderCards(state.response.hits).forEach((element, index) => {
|
|
288
295
|
controller.observeResult(element, index); // viewport impression
|
|
@@ -295,16 +302,20 @@ input.oninput = () => controller.setQuery(input.value); // debounced
|
|
|
295
302
|
form.onsubmit = () => controller.submit(input.value); // immediate
|
|
296
303
|
nextButton.onclick = () => controller.setPage(controller.getState().page + 1);
|
|
297
304
|
retryButton.onclick = () => controller.retry();
|
|
298
|
-
//
|
|
305
|
+
// Cancel requests and remove observers on unmount.
|
|
299
306
|
controller.dispose();
|
|
300
307
|
```
|
|
301
308
|
|
|
302
|
-
The
|
|
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:
|
|
303
314
|
|
|
304
|
-
-
|
|
305
|
-
-
|
|
306
|
-
-
|
|
307
|
-
-
|
|
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.
|
|
308
319
|
|
|
309
320
|
The raw JavaScript reference adapter lives in [`packages/search-example`](../search-example).
|
|
310
321
|
|
|
@@ -530,7 +541,8 @@ interface AssistantEvent {
|
|
|
530
541
|
payload: {
|
|
531
542
|
// Common fields (included in all events)
|
|
532
543
|
date: string; // ISO timestamp
|
|
533
|
-
locale: string; //
|
|
544
|
+
locale: string; // BCP-47 locale
|
|
545
|
+
currency: string; // Current currency
|
|
534
546
|
url: string; // Current page URL
|
|
535
547
|
userId?: string; // User ID if available
|
|
536
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
67
|
-
*
|
|
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
|
}
|
package/dist/Dialog.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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,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,WAAW,EACX,SAAS,EACT,KAAK,EACL,MAAM,EACN,uBAAuB,EACvB,gBAAgB,EAChB,OAAO,GACR,EAAE,iBAAiB;
|
|
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
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
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
|
-
*
|
|
101
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
194
|
-
//
|
|
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
|
|
222
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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: "
|
|
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",
|
|
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: "
|
|
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
|
-
//
|
|
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
|
|
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
|
-
{
|
|
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("
|
|
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.
|
|
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
|
-
//
|
|
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);
|
|
@@ -288,4 +301,96 @@ describe("createSearchController", () => {
|
|
|
288
301
|
expect(search).toHaveBeenCalledTimes(1);
|
|
289
302
|
expect(controller.getState().status).toBe(SearchStatus.LOADING);
|
|
290
303
|
});
|
|
304
|
+
it("requests the sections on their first page with the products and exposes their entries", async () => {
|
|
305
|
+
const controller = createSearchController({
|
|
306
|
+
search,
|
|
307
|
+
language: "fr",
|
|
308
|
+
currency: "USD",
|
|
309
|
+
analytics: {
|
|
310
|
+
surface: "search_page",
|
|
311
|
+
trackViewSearchResults,
|
|
312
|
+
trackSelectSearchResult,
|
|
313
|
+
},
|
|
314
|
+
sections: [{ index: "collections", hitsPerPage: 5 }],
|
|
315
|
+
});
|
|
316
|
+
const collections = {
|
|
317
|
+
...response().results[0],
|
|
318
|
+
index: "collections_fr_usd",
|
|
319
|
+
hits: [{ objectID: "c1" }],
|
|
320
|
+
nbHits: 1,
|
|
321
|
+
};
|
|
322
|
+
search.mockResolvedValueOnce({
|
|
323
|
+
results: [...response({ index: "products_fr_usd" }).results, collections],
|
|
324
|
+
});
|
|
325
|
+
controller.submit("shoes");
|
|
326
|
+
await settle();
|
|
327
|
+
expect(search.mock.calls[0][0].requests).toEqual([
|
|
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
|
+
},
|
|
340
|
+
]);
|
|
341
|
+
expect(controller.getState().response?.index).toBe("products_fr_usd");
|
|
342
|
+
expect(controller.getState().sections).toEqual({ collections });
|
|
343
|
+
});
|
|
344
|
+
it("resolves a sections function per request and leaves a missing entry out", async () => {
|
|
345
|
+
let enabled = false;
|
|
346
|
+
const controller = createSearchController({
|
|
347
|
+
search,
|
|
348
|
+
language: "fr",
|
|
349
|
+
currency: "EUR",
|
|
350
|
+
analytics: {
|
|
351
|
+
surface: "search_page",
|
|
352
|
+
trackViewSearchResults,
|
|
353
|
+
trackSelectSearchResult,
|
|
354
|
+
},
|
|
355
|
+
sections: () => (enabled ? [{ index: "collections" }] : []),
|
|
356
|
+
});
|
|
357
|
+
search.mockResolvedValue(response());
|
|
358
|
+
controller.submit("shoes");
|
|
359
|
+
await settle();
|
|
360
|
+
enabled = true;
|
|
361
|
+
controller.submit("boots");
|
|
362
|
+
await settle();
|
|
363
|
+
expect(search.mock.calls[0][0].requests).toHaveLength(1);
|
|
364
|
+
expect(search.mock.calls[1][0].requests).toHaveLength(2);
|
|
365
|
+
expect(search.mock.calls[1][0].requests[1]).toMatchObject({
|
|
366
|
+
indexName: "collections_fr_eur",
|
|
367
|
+
hitsPerPage: 12,
|
|
368
|
+
});
|
|
369
|
+
expect(controller.getState().sections).toEqual({});
|
|
370
|
+
});
|
|
371
|
+
it("enters the error state when the sections resolver throws", async () => {
|
|
372
|
+
const failure = new Error("entitlement lookup failed");
|
|
373
|
+
const controller = createSearchController({
|
|
374
|
+
search,
|
|
375
|
+
language: "fr",
|
|
376
|
+
currency: "EUR",
|
|
377
|
+
analytics: {
|
|
378
|
+
surface: "search_page",
|
|
379
|
+
trackViewSearchResults,
|
|
380
|
+
trackSelectSearchResult,
|
|
381
|
+
},
|
|
382
|
+
sections: () => {
|
|
383
|
+
throw failure;
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
controller.submit("shoes");
|
|
387
|
+
await settle();
|
|
388
|
+
expect(search).not.toHaveBeenCalled();
|
|
389
|
+
expect(controller.getState()).toMatchObject({
|
|
390
|
+
status: SearchStatus.ERROR,
|
|
391
|
+
error: failure,
|
|
392
|
+
response: undefined,
|
|
393
|
+
sections: undefined,
|
|
394
|
+
});
|
|
395
|
+
});
|
|
291
396
|
});
|