@jotul/jotul-widgets 1.2.5 → 2.0.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 (60) hide show
  1. package/README.md +112 -28
  2. package/dist/JotulWidget.css +1 -1
  3. package/dist/JotulWidget.d.ts +1 -1
  4. package/dist/JotulWidget.js +325 -132
  5. package/dist/analytics/WidgetTrackingContext.d.ts +14 -0
  6. package/dist/analytics/WidgetTrackingContext.js +5 -0
  7. package/dist/analytics/gtm.d.ts +7 -0
  8. package/dist/analytics/gtm.js +17 -0
  9. package/dist/analytics/widgetTracking.d.ts +54 -0
  10. package/dist/analytics/widgetTracking.js +144 -0
  11. package/dist/api.d.ts +27 -1
  12. package/dist/api.js +74 -0
  13. package/dist/components/FindDealerDrawerWidget.d.ts +7 -4
  14. package/dist/components/FindDealerDrawerWidget.js +17 -14
  15. package/dist/components/InquiryField.d.ts +3 -1
  16. package/dist/components/InquiryField.js +19 -2
  17. package/dist/components/InquirySelectField.d.ts +13 -0
  18. package/dist/components/InquirySelectField.js +5 -0
  19. package/dist/components/ProductPageWidget.d.ts +7 -4
  20. package/dist/components/ProductPageWidget.js +12 -14
  21. package/dist/components/TurnstileField.d.ts +7 -0
  22. package/dist/components/TurnstileField.js +48 -0
  23. package/dist/components/WarrantyFormWidget.d.ts +12 -0
  24. package/dist/components/WarrantyFormWidget.js +98 -0
  25. package/dist/components/product-page/DealerList.d.ts +1 -1
  26. package/dist/components/product-page/DealerList.js +14 -6
  27. package/dist/components/product-page/InquiryForm.d.ts +6 -2
  28. package/dist/components/product-page/InquiryForm.js +21 -3
  29. package/dist/constants/turnstile.d.ts +8 -0
  30. package/dist/constants/turnstile.js +19 -0
  31. package/dist/hooks/useTurnstileSiteKey.d.ts +1 -0
  32. package/dist/hooks/useTurnstileSiteKey.js +38 -0
  33. package/dist/i18n/locales/cz.json +34 -1
  34. package/dist/i18n/locales/de.json +34 -1
  35. package/dist/i18n/locales/en.json +34 -1
  36. package/dist/i18n/locales/fi.json +34 -1
  37. package/dist/i18n/locales/fr.json +34 -1
  38. package/dist/i18n/locales/nl.json +34 -1
  39. package/dist/i18n/locales/no.json +34 -1
  40. package/dist/i18n/locales/pl.json +34 -1
  41. package/dist/i18n/locales/se.json +34 -1
  42. package/dist/i18n/widgetStrings.d.ts +33 -0
  43. package/dist/index.d.ts +4 -0
  44. package/dist/index.js +3 -0
  45. package/dist/turnstile.d.ts +18 -0
  46. package/dist/turnstile.js +31 -0
  47. package/dist/types.d.ts +56 -0
  48. package/dist/utils/inquiryCategories.d.ts +8 -0
  49. package/dist/utils/inquiryCategories.js +24 -0
  50. package/dist/utils/inquirySubmit.d.ts +24 -0
  51. package/dist/utils/inquirySubmit.js +35 -0
  52. package/dist/utils/urlDealerId.d.ts +2 -0
  53. package/dist/utils/urlDealerId.js +5 -0
  54. package/dist/utils/usMarket.d.ts +2 -0
  55. package/dist/utils/usMarket.js +9 -0
  56. package/dist/utils/warrantyForm.d.ts +38 -0
  57. package/dist/utils/warrantyForm.js +80 -0
  58. package/dist/utils.d.ts +10 -1
  59. package/dist/utils.js +46 -3
  60. package/package.json +5 -2
@@ -6,8 +6,16 @@ import { createPortal } from 'react-dom';
6
6
  import { FinderSearchRowSkeleton } from './components/FinderSearchRowSkeleton';
7
7
  import { DealerCardSkeleton } from './components/DealerCardSkeleton';
8
8
  import { DEFAULT_WIDGET_LOCALE_TAG, resolveWidgetUiLocale, WIDGET_STRINGS, } from './i18n/widgetStrings';
9
- import { checkWidgetAuthorization, searchLocationSuggestions, searchDealersByCoordinates, searchDealersByPostalCode, } from './api';
10
- import { createInquiryFormValues, getSafeWidgetErrorMessage, isDealerInSearchResult, isValidEmail, isWidgetType, renderReadyState, } from './utils';
9
+ import { checkWidgetAuthorization, fetchDealerById, searchLocationSuggestions, searchDealersByCoordinates, searchDealersByPostalCode, submitInquiry, } from './api';
10
+ import { createInquiryFormValues, getDealerId, getDealerName, getSafeWidgetErrorMessage, isDealerInSearchResult, normalizeWidgetFilterList, validateInquiryFormValues, isWidgetType, renderReadyState, } from './utils';
11
+ import { inquiryCategoryLabel } from './utils/inquiryCategories';
12
+ import { inquiryFormValuesToApiPayload } from './utils/inquirySubmit';
13
+ import { useTurnstileSiteKey } from './hooks/useTurnstileSiteKey';
14
+ import { readDealerIdFromUrlSearch } from './utils/urlDealerId';
15
+ import { WidgetTrackingContext } from './analytics/WidgetTrackingContext';
16
+ import { createWidgetTracker, } from './analytics/widgetTracking';
17
+ import { ensureDataLayer } from './analytics/gtm';
18
+ import { WarrantyFormWidget } from './components/WarrantyFormWidget';
11
19
  function renderButton(buttonElement, onClick) {
12
20
  if (isValidElement(buttonElement)) {
13
21
  return cloneElement(buttonElement, { onClick });
@@ -19,6 +27,23 @@ function renderButton(buttonElement, onClick) {
19
27
  }
20
28
  }, style: { cursor: 'pointer', display: 'inline-block' }, children: buttonElement }));
21
29
  }
30
+ function renderLoadingButton(buttonElement, loadingText) {
31
+ if (isValidElement(buttonElement)) {
32
+ return cloneElement(buttonElement, {
33
+ disabled: true,
34
+ 'aria-busy': true,
35
+ children: loadingText,
36
+ });
37
+ }
38
+ return (_jsx("span", { role: "button", "aria-busy": "true", style: { cursor: 'wait', display: 'inline-block' }, children: loadingText }));
39
+ }
40
+ function resolveButtonLoading(button, buttonLoading, loadingText) {
41
+ if (buttonLoading != null)
42
+ return buttonLoading;
43
+ if (button == null)
44
+ return null;
45
+ return renderLoadingButton(button, loadingText);
46
+ }
22
47
  export { DEFAULT_WIDGET_LOCALE_TAG, normalizeWidgetLocale, resolveWidgetUiLocale, } from './i18n/widgetStrings';
23
48
  export { checkWidgetAuthorization, searchLocationSuggestions, searchDealersByCoordinates, searchDealersByPostalCode, };
24
49
  const GEO_PERMISSION_DENIED = 1;
@@ -36,20 +61,16 @@ const MARKET_FALLBACK_CENTER = {
36
61
  FI: [60.1699, 24.9384],
37
62
  DE: [52.52, 13.405],
38
63
  };
39
- export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, productName, locale: localeProp, markets: marketsProp, scope, brands, campaignSlug, styling, button, buttonLoading, widgetRef, }) {
40
- const apiMarkets = useMemo(() => {
41
- if (marketsProp == null)
42
- return undefined;
43
- if (!Array.isArray(marketsProp))
44
- return undefined;
45
- const valid = marketsProp
46
- .map((m) => m?.trim().toUpperCase())
47
- .filter((m) => m.length === 2 && /^[A-Z]{2}$/.test(m));
48
- return valid.length > 0 ? valid : undefined;
49
- }, [marketsProp]);
64
+ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, productName, locale: localeProp, market: marketProp, markets: marketsProp, scope, brands, campaignSlug, styling, button, buttonLoading, widgetRef, pageType: pageTypeProp, productId, warrantyEndpoint, submissionEndpoint = '/api/jotul/submission', turnstileSiteKey: turnstileSiteKeyProp, turnstileConfigEndpoint, }) {
65
+ const apiMarkets = useMemo(() => normalizeWidgetFilterList(marketsProp, (market) => {
66
+ const upper = market.toUpperCase();
67
+ return /^[A-Z]{2}$/.test(upper) ? upper : '';
68
+ }), [marketsProp]);
69
+ const apiBrands = useMemo(() => normalizeWidgetFilterList(brands), [brands]);
50
70
  const firstMarket = apiMarkets?.[0];
51
71
  const resolvedUiLocale = useMemo(() => resolveWidgetUiLocale(localeProp, firstMarket), [localeProp, firstMarket]);
52
72
  const t = WIDGET_STRINGS[resolvedUiLocale];
73
+ const resolvedTurnstileSiteKey = useTurnstileSiteKey(turnstileSiteKeyProp, turnstileConfigEndpoint);
53
74
  const apiLocaleTag = useMemo(() => (localeProp?.trim() ? localeProp.trim() : DEFAULT_WIDGET_LOCALE_TAG), [localeProp]);
54
75
  const [auth, setAuth] = useState(null);
55
76
  const [isLoading, setIsLoading] = useState(false);
@@ -65,23 +86,92 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
65
86
  const [isOpen, setIsOpen] = useState(false);
66
87
  const [shouldAutoLocateAfterAuth, setShouldAutoLocateAfterAuth] = useState(false);
67
88
  const [selectedDealerName, setSelectedDealerName] = useState(null);
89
+ const [selectedDealerId, setSelectedDealerId] = useState(null);
68
90
  const [inquiryValues, setInquiryValues] = useState(null);
69
91
  const [inquiryError, setInquiryError] = useState(null);
92
+ const [isSubmittingInquiry, setIsSubmittingInquiry] = useState(false);
93
+ const [turnstileResetKey, setTurnstileResetKey] = useState(0);
70
94
  const [isInquirySubmitted, setIsInquirySubmitted] = useState(false);
71
- const [mounted, setMounted] = useState(false);
72
95
  const [FindDealerDrawerWidgetComp, setFindDealerDrawerWidgetComp] = useState(null);
73
96
  const [ProductPageWidgetComp, setProductPageWidgetComp] = useState(null);
74
97
  const [isComponentLoading, setIsComponentLoading] = useState(false);
98
+ const [mounted, setMounted] = useState(false);
99
+ const [urlDealerId, setUrlDealerId] = useState(null);
100
+ const [isLoadingUrlDealer, setIsLoadingUrlDealer] = useState(false);
75
101
  const autocompleteCacheRef = useRef(new Map());
102
+ const locationQueryRef = useRef('');
103
+ const pendingDealerSearchTermRef = useRef(null);
104
+ const urlDealerHandledRef = useRef(false);
105
+ const trackingStateRef = useRef({
106
+ inquiryValues: null,
107
+ isInquirySubmitted: false,
108
+ selectedDealerId: null,
109
+ selectedDealerName: null,
110
+ });
76
111
  const productPageCampaignSlug = type === 'productPage' ? campaignSlug : undefined;
112
+ const widgetFormId = type === 'findDealerDrawer'
113
+ ? 'findDealerDrawer'
114
+ : type === 'productPage'
115
+ ? 'productPage'
116
+ : null;
117
+ const resolvedPageType = pageTypeProp?.trim() ||
118
+ (widgetFormId === 'findDealerDrawer' ? 'page' : 'product_page');
119
+ const tracker = useMemo(() => {
120
+ if (widgetFormId == null)
121
+ return null;
122
+ return createWidgetTracker({
123
+ formId: widgetFormId,
124
+ language: apiLocaleTag,
125
+ market: firstMarket,
126
+ pageType: resolvedPageType,
127
+ productId: widgetFormId === 'productPage' ? productId?.trim() || undefined : undefined,
128
+ productName: widgetFormId === 'productPage' ? productName?.trim() || undefined : undefined,
129
+ }, () => trackingStateRef.current);
130
+ }, [
131
+ widgetFormId,
132
+ apiLocaleTag,
133
+ firstMarket,
134
+ resolvedPageType,
135
+ productId,
136
+ productName,
137
+ ]);
138
+ trackingStateRef.current = {
139
+ inquiryValues,
140
+ isInquirySubmitted,
141
+ selectedDealerId,
142
+ selectedDealerName,
143
+ };
144
+ useEffect(() => {
145
+ locationQueryRef.current = locationQuery;
146
+ }, [locationQuery]);
147
+ useEffect(() => {
148
+ if (!isOpen || tracker == null)
149
+ return;
150
+ ensureDataLayer();
151
+ }, [isOpen, tracker]);
152
+ useEffect(() => {
153
+ if (!isOpen || tracker == null || searchResult == null || isSearching)
154
+ return;
155
+ const resultCount = searchResult.ok === true
156
+ ? (searchResult.total ?? searchResult.dealers?.length ?? 0)
157
+ : 0;
158
+ const pendingSearchTerm = pendingDealerSearchTermRef.current;
159
+ if (pendingSearchTerm != null) {
160
+ tracker.trackDealerSearch(pendingSearchTerm, resultCount);
161
+ pendingDealerSearchTermRef.current = null;
162
+ }
163
+ if (searchResult.ok === true && resultCount > 0) {
164
+ tracker.trackDealerListView(resultCount);
165
+ }
166
+ }, [isOpen, tracker, searchResult, isSearching]);
77
167
  const dealerSearchOptions = useMemo(() => ({
78
168
  endpoint,
79
169
  locale: apiLocaleTag,
80
170
  markets: apiMarkets,
81
171
  scope,
82
- brands,
172
+ brands: apiBrands,
83
173
  campaignSlug: productPageCampaignSlug,
84
- }), [apiLocaleTag, apiMarkets, brands, endpoint, productPageCampaignSlug, scope]);
174
+ }), [apiLocaleTag, apiMarkets, apiBrands, endpoint, productPageCampaignSlug, scope]);
85
175
  const runDealerSearchByCoordinates = useCallback(async (latitude, longitude) => {
86
176
  setLocationError(null);
87
177
  setIsSearching(true);
@@ -94,7 +184,11 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
94
184
  finally {
95
185
  setIsSearching(false);
96
186
  }
97
- }, [dealerSearchOptions, scope]);
187
+ }, [dealerSearchOptions]);
188
+ const runUserDealerSearchByCoordinates = useCallback(async (latitude, longitude, searchTerm) => {
189
+ pendingDealerSearchTermRef.current = searchTerm;
190
+ await runDealerSearchByCoordinates(latitude, longitude);
191
+ }, [runDealerSearchByCoordinates]);
98
192
  const runFallbackDealerSearch = useCallback(() => {
99
193
  const fallbackCenter = (firstMarket != null ? MARKET_FALLBACK_CENTER[firstMarket] : undefined) ??
100
194
  MARKET_FALLBACK_CENTER.NO;
@@ -166,7 +260,7 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
166
260
  };
167
261
  }, [apiLocaleTag, firstMarket, productPageCampaignSlug, dealerSearchOptions, locationQuery]);
168
262
  useEffect(() => {
169
- if ((type === 'productPage' || type === 'findDealerDrawer') && !isOpen)
263
+ if ((type === 'productPage' || type === 'findDealerDrawer') && !isOpen && !urlDealerId)
170
264
  return;
171
265
  if (auth != null)
172
266
  return;
@@ -183,7 +277,7 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
183
277
  return () => {
184
278
  cancelled = true;
185
279
  };
186
- }, [auth, endpoint, isOpen, type]);
280
+ }, [auth, endpoint, isOpen, type, urlDealerId]);
187
281
  useEffect(() => {
188
282
  if (!shouldAutoLocateAfterAuth)
189
283
  return;
@@ -209,6 +303,21 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
209
303
  return 'typeReady';
210
304
  }, [type]);
211
305
  const widgetType = isWidgetType(type) ? type : undefined;
306
+ useEffect(() => {
307
+ if (widgetType !== 'findDealerDrawer' || typeof window === 'undefined')
308
+ return;
309
+ const syncFromUrl = () => {
310
+ setUrlDealerId(readDealerIdFromUrlSearch(window.location.search));
311
+ };
312
+ syncFromUrl();
313
+ window.addEventListener('popstate', syncFromUrl);
314
+ return () => {
315
+ window.removeEventListener('popstate', syncFromUrl);
316
+ };
317
+ }, [widgetType]);
318
+ useEffect(() => {
319
+ urlDealerHandledRef.current = false;
320
+ }, [urlDealerId]);
212
321
  useEffect(() => {
213
322
  if (!isOpen)
214
323
  return;
@@ -243,6 +352,11 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
243
352
  setLocationQuery('');
244
353
  setLocationSuggestions([]);
245
354
  setIsSuggestionListOpen(false);
355
+ setSelectedDealerId(null);
356
+ pendingDealerSearchTermRef.current = null;
357
+ tracker?.resetSession();
358
+ ensureDataLayer();
359
+ tracker?.trackWidgetOpened();
246
360
  setIsOpen(true);
247
361
  if (auth?.ok && auth.authorized === true && !isLoading) {
248
362
  setShouldAutoLocateAfterAuth(false);
@@ -251,34 +365,160 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
251
365
  else {
252
366
  setShouldAutoLocateAfterAuth(true);
253
367
  }
254
- }, [auth, isLoading, runLocationSearch]);
368
+ }, [auth, isLoading, runLocationSearch, tracker]);
255
369
  const closeDealerWidget = useCallback(() => {
256
370
  setIsOpen(false);
257
371
  setLocationSuggestions([]);
258
372
  setIsSuggestionListOpen(false);
259
373
  setIsSearchingSuggestions(false);
260
374
  setMapSearchResult(null);
261
- }, []);
375
+ setSelectedDealerId(null);
376
+ pendingDealerSearchTermRef.current = null;
377
+ tracker?.resetSession();
378
+ }, [tracker]);
262
379
  useImperativeHandle(widgetRef, () => ({
263
380
  open: openProductPageWidget,
264
381
  close: closeDealerWidget,
265
382
  isOpen,
266
383
  }), [openProductPageWidget, closeDealerWidget, isOpen]);
384
+ const handleInquiryClose = useCallback(() => {
385
+ setSelectedDealerName(null);
386
+ setSelectedDealerId(null);
387
+ setInquiryValues(null);
388
+ setInquiryError(null);
389
+ }, []);
390
+ const handleInquirySubmit = useCallback(async (turnstileToken) => {
391
+ if (inquiryValues == null || widgetFormId == null)
392
+ return;
393
+ const validationError = validateInquiryFormValues(inquiryValues);
394
+ if (validationError) {
395
+ setInquiryError(validationError.type === 'invalid_email'
396
+ ? t.formValidationEmail
397
+ : t.formValidationRequired);
398
+ tracker?.trackFormError(validationError);
399
+ return;
400
+ }
401
+ setInquiryError(null);
402
+ setIsSubmittingInquiry(true);
403
+ try {
404
+ const payload = inquiryFormValuesToApiPayload(inquiryValues, {
405
+ formId: widgetFormId,
406
+ categoryLabel: inquiryCategoryLabel(inquiryValues.requestCategory, t),
407
+ domain: typeof window !== 'undefined' ? window.location.hostname : undefined,
408
+ });
409
+ if (turnstileToken?.trim()) {
410
+ payload.turnstileToken = turnstileToken.trim();
411
+ }
412
+ const result = await submitInquiry(payload, { endpoint: submissionEndpoint });
413
+ if (result.ok !== true) {
414
+ setInquiryError(typeof result.error === 'string' ? result.error : t.genericWidgetError);
415
+ setTurnstileResetKey((current) => current + 1);
416
+ return;
417
+ }
418
+ setIsInquirySubmitted(true);
419
+ setSelectedDealerName(null);
420
+ setSelectedDealerId(null);
421
+ setInquiryValues(null);
422
+ setTurnstileResetKey((current) => current + 1);
423
+ tracker?.trackSuccessfulSubmit();
424
+ }
425
+ catch {
426
+ setInquiryError(t.genericWidgetError);
427
+ setTurnstileResetKey((current) => current + 1);
428
+ }
429
+ finally {
430
+ setIsSubmittingInquiry(false);
431
+ }
432
+ }, [
433
+ inquiryValues,
434
+ widgetFormId,
435
+ t,
436
+ tracker,
437
+ submissionEndpoint,
438
+ ]);
439
+ const handleStartInquiry = useCallback((dealer) => {
440
+ const dealerName = getDealerName(dealer, t.unknownDealer);
441
+ const dealerId = getDealerId(dealer);
442
+ setSelectedDealerName(dealerName);
443
+ setSelectedDealerId(dealerId ?? null);
444
+ setInquiryValues(createInquiryFormValues(productName, dealer, t.unknownDealer, apiMarkets?.[0]));
445
+ setInquiryError(null);
446
+ setIsInquirySubmitted(false);
447
+ tracker?.trackFormStart(dealerId, dealerName);
448
+ }, [apiMarkets, productName, t.unknownDealer, tracker]);
449
+ const openDrawerFromUrlDealer = useCallback(async (dealerId) => {
450
+ setIsLoadingUrlDealer(true);
451
+ try {
452
+ const result = await fetchDealerById(dealerId, dealerSearchOptions);
453
+ if (result.ok !== true || result.dealer == null) {
454
+ return;
455
+ }
456
+ setShouldAutoLocateAfterAuth(false);
457
+ setLocationError(null);
458
+ setSearchResult(null);
459
+ setMapSearchResult(null);
460
+ setLocationQuery('');
461
+ setLocationSuggestions([]);
462
+ setIsSuggestionListOpen(false);
463
+ pendingDealerSearchTermRef.current = null;
464
+ tracker?.resetSession();
465
+ ensureDataLayer();
466
+ tracker?.trackWidgetOpened();
467
+ const dealer = result.dealer;
468
+ const latitude = typeof dealer.latitude === 'number' && Number.isFinite(dealer.latitude)
469
+ ? dealer.latitude
470
+ : undefined;
471
+ const longitude = typeof dealer.longitude === 'number' && Number.isFinite(dealer.longitude)
472
+ ? dealer.longitude
473
+ : undefined;
474
+ const singleResult = {
475
+ ok: true,
476
+ type: 'geolocation',
477
+ total: 1,
478
+ dealers: [dealer],
479
+ ...(latitude != null && longitude != null
480
+ ? { origin: { latitude, longitude } }
481
+ : {}),
482
+ };
483
+ setSearchResult(singleResult);
484
+ setMapSearchResult(singleResult);
485
+ setIsOpen(true);
486
+ handleStartInquiry(dealer);
487
+ }
488
+ finally {
489
+ setIsLoadingUrlDealer(false);
490
+ }
491
+ }, [dealerSearchOptions, handleStartInquiry, tracker]);
492
+ useEffect(() => {
493
+ if (widgetType !== 'findDealerDrawer' || urlDealerId == null)
494
+ return;
495
+ if (auth == null || isLoading)
496
+ return;
497
+ if (!auth.ok || !auth.authorized)
498
+ return;
499
+ if (urlDealerHandledRef.current)
500
+ return;
501
+ urlDealerHandledRef.current = true;
502
+ void openDrawerFromUrlDealer(urlDealerId);
503
+ }, [auth, isLoading, openDrawerFromUrlDealer, urlDealerId, widgetType]);
267
504
  const shellClass = 'jwi-box-border jwi-flex jwi-w-[540px] jwi-max-w-full jwi-flex-col jwi-font-sans jwi-text-[#111111]';
268
505
  const rootClass = className != null && className !== '' ? `${shellClass} ${className}` : shellClass;
269
506
  if (typeState !== 'typeReady') {
270
507
  return _jsx("div", { className: rootClass, children: t.invalidWidgetTypeError });
271
508
  }
509
+ if (widgetType === 'warrantyForm') {
510
+ return (_jsx(WarrantyFormWidget, { endpoint: warrantyEndpoint, className: className, locale: localeProp, market: marketProp, styling: styling, turnstileSiteKey: turnstileSiteKeyProp, turnstileConfigEndpoint: turnstileConfigEndpoint }));
511
+ }
272
512
  if (widgetType === 'productPage' && !isOpen) {
273
513
  if (button == null)
274
514
  return null;
275
- return (_jsx("div", { className: rootClass, children: renderButton(button, openProductPageWidget) }));
515
+ return renderButton(button, openProductPageWidget);
276
516
  }
277
517
  const productPageAuthPending = widgetType === 'productPage' &&
278
518
  isOpen &&
279
519
  (auth === null || isLoading);
280
520
  if (productPageAuthPending) {
281
- return (_jsx("div", { className: rootClass, children: buttonLoading ?? button }));
521
+ return resolveButtonLoading(button, buttonLoading, t.loading);
282
522
  }
283
523
  const waitingForAuth = auth === null &&
284
524
  !((widgetType === 'productPage' || widgetType === 'findDealerDrawer') &&
@@ -288,7 +528,13 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
288
528
  type !== 'findDealerDrawer') {
289
529
  return (_jsx("div", { className: rootClass, children: _jsx("div", { className: "jwi-flex jwi-w-full jwi-flex-col", children: _jsx(FinderSearchRowSkeleton, {}) }) }));
290
530
  }
291
- const deferAuthErrorUntilDrawerOpens = widgetType === 'findDealerDrawer' && !isOpen && auth === null;
531
+ const deferAuthErrorUntilDrawerOpens = widgetType === 'findDealerDrawer' && !isOpen && auth === null && urlDealerId == null;
532
+ if (urlDealerId != null &&
533
+ auth != null &&
534
+ !isLoading &&
535
+ (!auth.ok || !auth.authorized)) {
536
+ return _jsx("div", { className: rootClass, children: getSafeWidgetErrorMessage(auth?.error, t) });
537
+ }
292
538
  const deferAuthUntilFindDealerDrawerResolves = widgetType === 'findDealerDrawer' && isOpen && (auth === null || isLoading);
293
539
  if (!deferAuthErrorUntilDrawerOpens &&
294
540
  !deferAuthUntilFindDealerDrawerResolves &&
@@ -296,12 +542,12 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
296
542
  return _jsx("div", { className: rootClass, children: getSafeWidgetErrorMessage(auth?.error, t) });
297
543
  }
298
544
  if (widgetType === 'productPage') {
299
- return (_jsx("div", { className: rootClass, children: ProductPageWidgetComp != null && mounted
300
- ? createPortal(_jsx(ProductPageWidgetComp, { t: t, buttonStyling: styling?.button, borderStyling: styling?.border, markets: apiMarkets, scope: scope, isSearching: isSearching, locationError: locationError, searchResult: searchResult?.ok === false
545
+ return ProductPageWidgetComp != null && mounted && tracker != null
546
+ ? createPortal(_jsx(WidgetTrackingContext.Provider, { value: tracker, children: _jsx(ProductPageWidgetComp, { t: t, buttonStyling: styling?.button, borderStyling: styling?.border, markets: apiMarkets, scope: scope, isSearching: isSearching, locationError: locationError, searchResult: searchResult?.ok === false
301
547
  ? { ...searchResult, error: getSafeWidgetErrorMessage(searchResult.error, t) }
302
548
  : searchResult, mapSearchResult: mapSearchResult?.ok === false
303
549
  ? { ...mapSearchResult, error: getSafeWidgetErrorMessage(mapSearchResult.error, t) }
304
- : mapSearchResult, inquiryValues: inquiryValues, inquiryError: inquiryError, isInquirySubmitted: isInquirySubmitted, selectedDealerName: selectedDealerName, isManualSearchEnabled: isManualLocationSearchEnabled, query: locationQuery, suggestions: locationSuggestions, suggestionsOpen: isSuggestionListOpen, isSuggestionsLoading: isSearchingSuggestions, onQueryChange: (value) => {
550
+ : mapSearchResult, inquiryValues: inquiryValues, inquiryError: inquiryError, isSubmittingInquiry: isSubmittingInquiry, turnstileSiteKey: resolvedTurnstileSiteKey, turnstileResetKey: turnstileResetKey, isInquirySubmitted: isInquirySubmitted, selectedDealerName: selectedDealerName, isManualSearchEnabled: isManualLocationSearchEnabled, query: locationQuery, suggestions: locationSuggestions, suggestionsOpen: isSuggestionListOpen, isSuggestionsLoading: isSearchingSuggestions, onQueryChange: (value) => {
305
551
  setLocationQuery(value);
306
552
  const trimmed = value.trim();
307
553
  setIsSuggestionListOpen(trimmed.length > 0);
@@ -324,60 +570,34 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
324
570
  setLocationSuggestions([]);
325
571
  setIsSearchingSuggestions(false);
326
572
  setIsSuggestionListOpen(false);
327
- await runDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude);
573
+ await runUserDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude, query);
328
574
  }, onSuggestionSelect: (suggestion) => {
329
575
  setLocationQuery(suggestion.label);
330
576
  setLocationSuggestions([]);
331
577
  setIsSearchingSuggestions(false);
332
578
  setIsSuggestionListOpen(false);
333
- void runDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude);
579
+ void runUserDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude, suggestion.label);
334
580
  }, onDismissSuggestions: () => {
335
581
  setLocationSuggestions([]);
336
582
  setIsSearchingSuggestions(false);
337
583
  setIsSuggestionListOpen(false);
338
- }, onInquiryClose: () => {
339
- setSelectedDealerName(null);
340
- setInquiryValues(null);
341
- setInquiryError(null);
342
- }, onInquirySubmit: () => {
343
- if (inquiryValues == null)
344
- return;
345
- const trimmedName = inquiryValues.name.trim();
346
- const trimmedEmail = inquiryValues.email.trim();
347
- const trimmedPhone = inquiryValues.phone.trim();
348
- if (!trimmedName || !trimmedEmail || !trimmedPhone) {
349
- setInquiryError(t.formValidationRequired);
350
- return;
351
- }
352
- if (!isValidEmail(trimmedEmail)) {
353
- setInquiryError(t.formValidationEmail);
354
- return;
355
- }
356
- setInquiryError(null);
357
- setIsInquirySubmitted(true);
358
- setSelectedDealerName(null);
359
- setInquiryValues(null);
360
- }, onInquiryFieldChange: (key, value) => setInquiryValues((current) => current == null ? current : { ...current, [key]: value }), onStartInquiry: (dealerName) => {
361
- setSelectedDealerName(dealerName);
362
- setInquiryValues(createInquiryFormValues(productName, dealerName));
363
- setInquiryError(null);
364
- setIsInquirySubmitted(false);
365
- }, onMapDealerSelect: (dealer) => {
584
+ }, onInquiryClose: handleInquiryClose, onInquirySubmit: handleInquirySubmit, onInquiryFieldChange: (key, value) => setInquiryValues((current) => current == null ? current : { ...current, [key]: value }), onStartInquiry: handleStartInquiry, onMapDealerSelect: (dealer) => {
366
585
  if (isDealerInSearchResult(dealer.dealerName, searchResult, t.unknownDealer))
367
586
  return;
368
- void runDealerSearchByCoordinates(dealer.latitude, dealer.longitude);
369
- }, onClosePopup: closeDealerWidget }), document.body)
370
- : null }));
587
+ void runUserDealerSearchByCoordinates(dealer.latitude, dealer.longitude, locationQueryRef.current.trim());
588
+ }, onClosePopup: closeDealerWidget }) }), document.body)
589
+ : null;
371
590
  }
372
591
  if (widgetType === 'findDealerDrawer') {
373
592
  const drawerLoading = isOpen &&
374
593
  (auth === null ||
375
594
  isLoading ||
376
595
  isComponentLoading ||
377
- (isSearching && searchResult == null && mapSearchResult == null));
378
- return (_jsxs("div", { className: rootClass, children: [button != null &&
596
+ isLoadingUrlDealer ||
597
+ (isSearching && searchResult == null && mapSearchResult == null && urlDealerId == null));
598
+ return (_jsxs(_Fragment, { children: [button != null &&
379
599
  (drawerLoading
380
- ? buttonLoading ?? button
600
+ ? resolveButtonLoading(button, buttonLoading, t.loading)
381
601
  : renderButton(button, openProductPageWidget)), mounted &&
382
602
  createPortal(_jsxs(_Fragment, { children: [_jsx("div", { className: "jwi-fixed jwi-inset-0 jwi-z-[2147483647] jwi-bg-black/35", style: {
383
603
  opacity: isOpen ? 1 : 0,
@@ -387,76 +607,49 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
387
607
  transform: isOpen ? 'translateX(0)' : 'translateX(100%)',
388
608
  transition: 'transform 300ms ease-out',
389
609
  willChange: 'transform',
390
- }, "aria-hidden": !isOpen, children: drawerLoading || FindDealerDrawerWidgetComp == null ? (_jsxs("div", { className: "jwi-flex jwi-h-full jwi-w-full jwi-bg-white", children: [_jsx("div", { className: "jwi-flex jwi-h-full jwi-min-h-0 jwi-w-1/2 jwi-flex-col jwi-overflow-hidden", children: _jsxs("div", { className: "jwi-flex jwi-h-full jwi-min-h-0 jwi-w-full jwi-flex-col jwi-gap-3 jwi-overflow-hidden jwi-bg-white jwi-p-6", children: [_jsx("div", { className: "jwi-h-12 jwi-w-full jwi-rounded-[10px] jwi-bg-[#ece8df] jwi-transition-opacity jwi-duration-300 jwi-ease-out" }), _jsx("div", { className: "jwi-h-5 jwi-w-48 jwi-rounded-full jwi-bg-[#ece8df] jwi-transition-opacity jwi-duration-300 jwi-ease-out" }), _jsxs("div", { className: "jwi-flex jwi-flex-col jwi-gap-4", children: [_jsx(DealerCardSkeleton, {}), _jsx(DealerCardSkeleton, {}), _jsx(DealerCardSkeleton, {})] })] }) }), _jsx("div", { className: "jwi-h-full jwi-w-1/2 jwi-bg-[#e8eef1]" })] })) : (_jsx(FindDealerDrawerWidgetComp, { t: t, buttonStyling: styling?.button, borderStyling: styling?.border, markets: apiMarkets, scope: scope, isSearching: isSearching, locationError: locationError, searchResult: searchResult?.ok === false
391
- ? { ...searchResult, error: getSafeWidgetErrorMessage(searchResult.error, t) }
392
- : searchResult, mapSearchResult: mapSearchResult?.ok === false
393
- ? { ...mapSearchResult, error: getSafeWidgetErrorMessage(mapSearchResult.error, t) }
394
- : mapSearchResult, inquiryValues: inquiryValues, inquiryError: inquiryError, isInquirySubmitted: isInquirySubmitted, selectedDealerName: selectedDealerName, isManualSearchEnabled: isManualLocationSearchEnabled, query: locationQuery, suggestions: locationSuggestions, suggestionsOpen: isSuggestionListOpen, isSuggestionsLoading: isSearchingSuggestions, onQueryChange: (value) => {
395
- setLocationQuery(value);
396
- const trimmed = value.trim();
397
- setIsSuggestionListOpen(trimmed.length > 0);
398
- if (trimmed.length < 3) {
610
+ }, "aria-hidden": !isOpen, children: drawerLoading || FindDealerDrawerWidgetComp == null ? (_jsxs("div", { className: "jwi-flex jwi-h-full jwi-w-full jwi-bg-white", children: [_jsx("div", { className: "jwi-flex jwi-h-full jwi-min-h-0 jwi-w-1/2 jwi-flex-col jwi-overflow-hidden", children: _jsxs("div", { className: "jwi-flex jwi-h-full jwi-min-h-0 jwi-w-full jwi-flex-col jwi-gap-3 jwi-overflow-hidden jwi-bg-white jwi-p-6", children: [_jsx("div", { className: "jwi-h-12 jwi-w-full jwi-rounded-[10px] jwi-bg-[#ece8df] jwi-transition-opacity jwi-duration-300 jwi-ease-out" }), _jsx("div", { className: "jwi-h-5 jwi-w-48 jwi-rounded-full jwi-bg-[#ece8df] jwi-transition-opacity jwi-duration-300 jwi-ease-out" }), _jsxs("div", { className: "jwi-flex jwi-flex-col jwi-gap-4", children: [_jsx(DealerCardSkeleton, {}), _jsx(DealerCardSkeleton, {}), _jsx(DealerCardSkeleton, {})] })] }) }), _jsx("div", { className: "jwi-h-full jwi-w-1/2 jwi-bg-[#e8eef1]" })] })) : (tracker != null ? (_jsx(WidgetTrackingContext.Provider, { value: tracker, children: _jsx(FindDealerDrawerWidgetComp, { t: t, buttonStyling: styling?.button, borderStyling: styling?.border, markets: apiMarkets, scope: scope, isSearching: isSearching, locationError: locationError, searchResult: searchResult?.ok === false
611
+ ? { ...searchResult, error: getSafeWidgetErrorMessage(searchResult.error, t) }
612
+ : searchResult, mapSearchResult: mapSearchResult?.ok === false
613
+ ? { ...mapSearchResult, error: getSafeWidgetErrorMessage(mapSearchResult.error, t) }
614
+ : mapSearchResult, inquiryValues: inquiryValues, inquiryError: inquiryError, isSubmittingInquiry: isSubmittingInquiry, turnstileSiteKey: resolvedTurnstileSiteKey, turnstileResetKey: turnstileResetKey, isInquirySubmitted: isInquirySubmitted, selectedDealerName: selectedDealerName, isManualSearchEnabled: isManualLocationSearchEnabled, query: locationQuery, suggestions: locationSuggestions, suggestionsOpen: isSuggestionListOpen, isSuggestionsLoading: isSearchingSuggestions, onQueryChange: (value) => {
615
+ setLocationQuery(value);
616
+ const trimmed = value.trim();
617
+ setIsSuggestionListOpen(trimmed.length > 0);
618
+ if (trimmed.length < 3) {
619
+ setLocationSuggestions([]);
620
+ }
621
+ }, onQuerySubmit: async (value) => {
622
+ const query = value.trim();
623
+ if (query.length < 3)
624
+ return;
625
+ setIsSearchingSuggestions(true);
626
+ const result = await searchLocationSuggestions(query, dealerSearchOptions);
627
+ const resolvedSuggestions = result.ok && Array.isArray(result.suggestions) ? result.suggestions : [];
628
+ setLocationSuggestions(resolvedSuggestions);
629
+ setIsSearchingSuggestions(false);
630
+ const suggestion = resolvedSuggestions[0];
631
+ if (!suggestion)
632
+ return;
633
+ setLocationQuery(suggestion.label);
634
+ setLocationSuggestions([]);
635
+ setIsSearchingSuggestions(false);
636
+ setIsSuggestionListOpen(false);
637
+ await runUserDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude, query);
638
+ }, onSuggestionSelect: (suggestion) => {
639
+ setLocationQuery(suggestion.label);
640
+ setLocationSuggestions([]);
641
+ setIsSearchingSuggestions(false);
642
+ setIsSuggestionListOpen(false);
643
+ void runUserDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude, suggestion.label);
644
+ }, onDismissSuggestions: () => {
399
645
  setLocationSuggestions([]);
400
- }
401
- }, onQuerySubmit: async (value) => {
402
- const query = value.trim();
403
- if (query.length < 3)
404
- return;
405
- setIsSearchingSuggestions(true);
406
- const result = await searchLocationSuggestions(query, dealerSearchOptions);
407
- const resolvedSuggestions = result.ok && Array.isArray(result.suggestions) ? result.suggestions : [];
408
- setLocationSuggestions(resolvedSuggestions);
409
- setIsSearchingSuggestions(false);
410
- const suggestion = resolvedSuggestions[0];
411
- if (!suggestion)
412
- return;
413
- setLocationQuery(suggestion.label);
414
- setLocationSuggestions([]);
415
- setIsSearchingSuggestions(false);
416
- setIsSuggestionListOpen(false);
417
- await runDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude);
418
- }, onSuggestionSelect: (suggestion) => {
419
- setLocationQuery(suggestion.label);
420
- setLocationSuggestions([]);
421
- setIsSearchingSuggestions(false);
422
- setIsSuggestionListOpen(false);
423
- void runDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude);
424
- }, onDismissSuggestions: () => {
425
- setLocationSuggestions([]);
426
- setIsSearchingSuggestions(false);
427
- setIsSuggestionListOpen(false);
428
- }, onInquiryClose: () => {
429
- setSelectedDealerName(null);
430
- setInquiryValues(null);
431
- setInquiryError(null);
432
- }, onInquirySubmit: () => {
433
- if (inquiryValues == null)
434
- return;
435
- const trimmedName = inquiryValues.name.trim();
436
- const trimmedEmail = inquiryValues.email.trim();
437
- const trimmedPhone = inquiryValues.phone.trim();
438
- if (!trimmedName || !trimmedEmail || !trimmedPhone) {
439
- setInquiryError(t.formValidationRequired);
440
- return;
441
- }
442
- if (!isValidEmail(trimmedEmail)) {
443
- setInquiryError(t.formValidationEmail);
444
- return;
445
- }
446
- setInquiryError(null);
447
- setIsInquirySubmitted(true);
448
- setSelectedDealerName(null);
449
- setInquiryValues(null);
450
- }, onInquiryFieldChange: (key, value) => setInquiryValues((current) => current == null ? current : { ...current, [key]: value }), onStartInquiry: (dealerName) => {
451
- setSelectedDealerName(dealerName);
452
- setInquiryValues(createInquiryFormValues(productName, dealerName));
453
- setInquiryError(null);
454
- setIsInquirySubmitted(false);
455
- }, onMapDealerSelect: (dealer) => {
456
- if (isDealerInSearchResult(dealer.dealerName, searchResult, t.unknownDealer))
457
- return;
458
- void runDealerSearchByCoordinates(dealer.latitude, dealer.longitude);
459
- }, onClose: closeDealerWidget })) })] }), document.body)] }));
646
+ setIsSearchingSuggestions(false);
647
+ setIsSuggestionListOpen(false);
648
+ }, onInquiryClose: handleInquiryClose, onInquirySubmit: handleInquirySubmit, onInquiryFieldChange: (key, value) => setInquiryValues((current) => current == null ? current : { ...current, [key]: value }), onStartInquiry: handleStartInquiry, onMapDealerSelect: (dealer) => {
649
+ if (isDealerInSearchResult(dealer.dealerName, searchResult, t.unknownDealer))
650
+ return;
651
+ void runUserDealerSearchByCoordinates(dealer.latitude, dealer.longitude, locationQueryRef.current.trim());
652
+ }, onClose: closeDealerWidget }) })) : null) })] }), document.body)] }));
460
653
  }
461
654
  return _jsx("div", { className: rootClass, children: renderReadyState(widgetType, t) });
462
655
  }
@@ -0,0 +1,14 @@
1
+ import type { WidgetTracker } from './widgetTracking';
2
+ export declare const WidgetTrackingContext: import("react").Context<{
3
+ resetSession(): void;
4
+ trackWidgetOpened(): void;
5
+ trackDealerSearch(searchTerm: string, resultCount: number): void;
6
+ trackDealerListView(resultCount: number): void;
7
+ trackDealerSelect(dealerId: string | undefined, dealerName: string): void;
8
+ trackDealerPhoneClick(dealerId: string | undefined, dealerName: string): void;
9
+ trackFormStart(dealerId: string | undefined, dealerName: string): void;
10
+ trackFormStepComplete(field: "name" | "email" | "phone" | "comment"): void;
11
+ trackFormError(error: import("./widgetTracking").InquiryValidationError): void;
12
+ trackSuccessfulSubmit(): void;
13
+ } | null>;
14
+ export declare function useWidgetTracking(): WidgetTracker | null;
@@ -0,0 +1,5 @@
1
+ import { createContext, useContext } from 'react';
2
+ export const WidgetTrackingContext = createContext(null);
3
+ export function useWidgetTracking() {
4
+ return useContext(WidgetTrackingContext);
5
+ }
@@ -0,0 +1,7 @@
1
+ declare global {
2
+ interface Window {
3
+ dataLayer?: Array<Record<string, unknown>>;
4
+ }
5
+ }
6
+ export declare function ensureDataLayer(): Array<Record<string, unknown>>;
7
+ export declare function pushDataLayerEvent(event: string, payload: Record<string, unknown>): void;