@jotul/jotul-widgets 1.2.6 → 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 +356 -163
  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 +13 -5
  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
95
  const [FindDealerDrawerWidgetComp, setFindDealerDrawerWidgetComp] = useState(null);
72
96
  const [ProductPageWidgetComp, setProductPageWidgetComp] = useState(null);
73
97
  const [isComponentLoading, setIsComponentLoading] = useState(false);
74
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,24 +365,150 @@ 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;
@@ -278,7 +518,7 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
278
518
  isOpen &&
279
519
  (auth === null || isLoading);
280
520
  if (productPageAuthPending) {
281
- return buttonLoading ?? button ?? null;
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,77 +542,50 @@ 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 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
301
- ? { ...searchResult, error: getSafeWidgetErrorMessage(searchResult.error, t) }
302
- : searchResult, mapSearchResult: mapSearchResult?.ok === false
303
- ? { ...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) => {
305
- setLocationQuery(value);
306
- const trimmed = value.trim();
307
- setIsSuggestionListOpen(trimmed.length > 0);
308
- if (trimmed.length < 3) {
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
547
+ ? { ...searchResult, error: getSafeWidgetErrorMessage(searchResult.error, t) }
548
+ : searchResult, mapSearchResult: mapSearchResult?.ok === false
549
+ ? { ...mapSearchResult, error: getSafeWidgetErrorMessage(mapSearchResult.error, t) }
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) => {
551
+ setLocationQuery(value);
552
+ const trimmed = value.trim();
553
+ setIsSuggestionListOpen(trimmed.length > 0);
554
+ if (trimmed.length < 3) {
555
+ setLocationSuggestions([]);
556
+ }
557
+ }, onQuerySubmit: async (value) => {
558
+ const query = value.trim();
559
+ if (query.length < 3)
560
+ return;
561
+ setIsSearchingSuggestions(true);
562
+ const result = await searchLocationSuggestions(query, dealerSearchOptions);
563
+ const resolvedSuggestions = result.ok && Array.isArray(result.suggestions) ? result.suggestions : [];
564
+ setLocationSuggestions(resolvedSuggestions);
565
+ setIsSearchingSuggestions(false);
566
+ const suggestion = resolvedSuggestions[0];
567
+ if (!suggestion)
568
+ return;
569
+ setLocationQuery(suggestion.label);
570
+ setLocationSuggestions([]);
571
+ setIsSearchingSuggestions(false);
572
+ setIsSuggestionListOpen(false);
573
+ await runUserDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude, query);
574
+ }, onSuggestionSelect: (suggestion) => {
575
+ setLocationQuery(suggestion.label);
309
576
  setLocationSuggestions([]);
310
- }
311
- }, onQuerySubmit: async (value) => {
312
- const query = value.trim();
313
- if (query.length < 3)
314
- return;
315
- setIsSearchingSuggestions(true);
316
- const result = await searchLocationSuggestions(query, dealerSearchOptions);
317
- const resolvedSuggestions = result.ok && Array.isArray(result.suggestions) ? result.suggestions : [];
318
- setLocationSuggestions(resolvedSuggestions);
319
- setIsSearchingSuggestions(false);
320
- const suggestion = resolvedSuggestions[0];
321
- if (!suggestion)
322
- return;
323
- setLocationQuery(suggestion.label);
324
- setLocationSuggestions([]);
325
- setIsSearchingSuggestions(false);
326
- setIsSuggestionListOpen(false);
327
- await runDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude);
328
- }, onSuggestionSelect: (suggestion) => {
329
- setLocationQuery(suggestion.label);
330
- setLocationSuggestions([]);
331
- setIsSearchingSuggestions(false);
332
- setIsSuggestionListOpen(false);
333
- void runDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude);
334
- }, onDismissSuggestions: () => {
335
- setLocationSuggestions([]);
336
- setIsSearchingSuggestions(false);
337
- 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) => {
366
- if (isDealerInSearchResult(dealer.dealerName, searchResult, t.unknownDealer))
367
- return;
368
- void runDealerSearchByCoordinates(dealer.latitude, dealer.longitude);
369
- }, onClosePopup: closeDealerWidget }), document.body)
577
+ setIsSearchingSuggestions(false);
578
+ setIsSuggestionListOpen(false);
579
+ void runUserDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude, suggestion.label);
580
+ }, onDismissSuggestions: () => {
581
+ setLocationSuggestions([]);
582
+ setIsSearchingSuggestions(false);
583
+ setIsSuggestionListOpen(false);
584
+ }, onInquiryClose: handleInquiryClose, onInquirySubmit: handleInquirySubmit, onInquiryFieldChange: (key, value) => setInquiryValues((current) => current == null ? current : { ...current, [key]: value }), onStartInquiry: handleStartInquiry, onMapDealerSelect: (dealer) => {
585
+ if (isDealerInSearchResult(dealer.dealerName, searchResult, t.unknownDealer))
586
+ return;
587
+ void runUserDealerSearchByCoordinates(dealer.latitude, dealer.longitude, locationQueryRef.current.trim());
588
+ }, onClosePopup: closeDealerWidget }) }), document.body)
370
589
  : null;
371
590
  }
372
591
  if (widgetType === 'findDealerDrawer') {
@@ -374,10 +593,11 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
374
593
  (auth === null ||
375
594
  isLoading ||
376
595
  isComponentLoading ||
377
- (isSearching && searchResult == null && mapSearchResult == null));
596
+ isLoadingUrlDealer ||
597
+ (isSearching && searchResult == null && mapSearchResult == null && urlDealerId == null));
378
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
  }