@jotul/jotul-widgets 1.2.6 → 2.1.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 +355 -165
  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 +59 -2
  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 +11 -1
  59. package/dist/utils.js +52 -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, normalizeDealerMarkets, 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,21 +61,14 @@ 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 apiLocaleTag = useMemo(() => (localeProp?.trim() ? localeProp.trim() : DEFAULT_WIDGET_LOCALE_TAG), [localeProp]);
66
+ const apiMarkets = useMemo(() => normalizeDealerMarkets(marketsProp), [marketsProp]);
67
+ const apiBrands = useMemo(() => normalizeWidgetFilterList(brands), [brands]);
50
68
  const firstMarket = apiMarkets?.[0];
51
- const resolvedUiLocale = useMemo(() => resolveWidgetUiLocale(localeProp, firstMarket), [localeProp, firstMarket]);
69
+ const resolvedUiLocale = useMemo(() => resolveWidgetUiLocale(localeProp), [localeProp]);
52
70
  const t = WIDGET_STRINGS[resolvedUiLocale];
53
- const apiLocaleTag = useMemo(() => (localeProp?.trim() ? localeProp.trim() : DEFAULT_WIDGET_LOCALE_TAG), [localeProp]);
71
+ const resolvedTurnstileSiteKey = useTurnstileSiteKey(turnstileSiteKeyProp, turnstileConfigEndpoint);
54
72
  const [auth, setAuth] = useState(null);
55
73
  const [isLoading, setIsLoading] = useState(false);
56
74
  const [searchResult, setSearchResult] = useState(null);
@@ -65,23 +83,92 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
65
83
  const [isOpen, setIsOpen] = useState(false);
66
84
  const [shouldAutoLocateAfterAuth, setShouldAutoLocateAfterAuth] = useState(false);
67
85
  const [selectedDealerName, setSelectedDealerName] = useState(null);
86
+ const [selectedDealerId, setSelectedDealerId] = useState(null);
68
87
  const [inquiryValues, setInquiryValues] = useState(null);
69
88
  const [inquiryError, setInquiryError] = useState(null);
89
+ const [isSubmittingInquiry, setIsSubmittingInquiry] = useState(false);
90
+ const [turnstileResetKey, setTurnstileResetKey] = useState(0);
70
91
  const [isInquirySubmitted, setIsInquirySubmitted] = useState(false);
71
92
  const [FindDealerDrawerWidgetComp, setFindDealerDrawerWidgetComp] = useState(null);
72
93
  const [ProductPageWidgetComp, setProductPageWidgetComp] = useState(null);
73
94
  const [isComponentLoading, setIsComponentLoading] = useState(false);
74
95
  const [mounted, setMounted] = useState(false);
96
+ const [urlDealerId, setUrlDealerId] = useState(null);
97
+ const [isLoadingUrlDealer, setIsLoadingUrlDealer] = useState(false);
75
98
  const autocompleteCacheRef = useRef(new Map());
99
+ const locationQueryRef = useRef('');
100
+ const pendingDealerSearchTermRef = useRef(null);
101
+ const urlDealerHandledRef = useRef(false);
102
+ const trackingStateRef = useRef({
103
+ inquiryValues: null,
104
+ isInquirySubmitted: false,
105
+ selectedDealerId: null,
106
+ selectedDealerName: null,
107
+ });
76
108
  const productPageCampaignSlug = type === 'productPage' ? campaignSlug : undefined;
109
+ const widgetFormId = type === 'findDealerDrawer'
110
+ ? 'findDealerDrawer'
111
+ : type === 'productPage'
112
+ ? 'productPage'
113
+ : null;
114
+ const resolvedPageType = pageTypeProp?.trim() ||
115
+ (widgetFormId === 'findDealerDrawer' ? 'page' : 'product_page');
116
+ const tracker = useMemo(() => {
117
+ if (widgetFormId == null)
118
+ return null;
119
+ return createWidgetTracker({
120
+ formId: widgetFormId,
121
+ language: apiLocaleTag,
122
+ market: firstMarket,
123
+ pageType: resolvedPageType,
124
+ productId: widgetFormId === 'productPage' ? productId?.trim() || undefined : undefined,
125
+ productName: widgetFormId === 'productPage' ? productName?.trim() || undefined : undefined,
126
+ }, () => trackingStateRef.current);
127
+ }, [
128
+ widgetFormId,
129
+ apiLocaleTag,
130
+ firstMarket,
131
+ resolvedPageType,
132
+ productId,
133
+ productName,
134
+ ]);
135
+ trackingStateRef.current = {
136
+ inquiryValues,
137
+ isInquirySubmitted,
138
+ selectedDealerId,
139
+ selectedDealerName,
140
+ };
141
+ useEffect(() => {
142
+ locationQueryRef.current = locationQuery;
143
+ }, [locationQuery]);
144
+ useEffect(() => {
145
+ if (!isOpen || tracker == null)
146
+ return;
147
+ ensureDataLayer();
148
+ }, [isOpen, tracker]);
149
+ useEffect(() => {
150
+ if (!isOpen || tracker == null || searchResult == null || isSearching)
151
+ return;
152
+ const resultCount = searchResult.ok === true
153
+ ? (searchResult.total ?? searchResult.dealers?.length ?? 0)
154
+ : 0;
155
+ const pendingSearchTerm = pendingDealerSearchTermRef.current;
156
+ if (pendingSearchTerm != null) {
157
+ tracker.trackDealerSearch(pendingSearchTerm, resultCount);
158
+ pendingDealerSearchTermRef.current = null;
159
+ }
160
+ if (searchResult.ok === true && resultCount > 0) {
161
+ tracker.trackDealerListView(resultCount);
162
+ }
163
+ }, [isOpen, tracker, searchResult, isSearching]);
77
164
  const dealerSearchOptions = useMemo(() => ({
78
165
  endpoint,
79
166
  locale: apiLocaleTag,
80
167
  markets: apiMarkets,
81
168
  scope,
82
- brands,
169
+ brands: apiBrands,
83
170
  campaignSlug: productPageCampaignSlug,
84
- }), [apiLocaleTag, apiMarkets, brands, endpoint, productPageCampaignSlug, scope]);
171
+ }), [apiLocaleTag, apiMarkets, apiBrands, endpoint, productPageCampaignSlug, scope]);
85
172
  const runDealerSearchByCoordinates = useCallback(async (latitude, longitude) => {
86
173
  setLocationError(null);
87
174
  setIsSearching(true);
@@ -94,7 +181,11 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
94
181
  finally {
95
182
  setIsSearching(false);
96
183
  }
97
- }, [dealerSearchOptions, scope]);
184
+ }, [dealerSearchOptions]);
185
+ const runUserDealerSearchByCoordinates = useCallback(async (latitude, longitude, searchTerm) => {
186
+ pendingDealerSearchTermRef.current = searchTerm;
187
+ await runDealerSearchByCoordinates(latitude, longitude);
188
+ }, [runDealerSearchByCoordinates]);
98
189
  const runFallbackDealerSearch = useCallback(() => {
99
190
  const fallbackCenter = (firstMarket != null ? MARKET_FALLBACK_CENTER[firstMarket] : undefined) ??
100
191
  MARKET_FALLBACK_CENTER.NO;
@@ -166,7 +257,7 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
166
257
  };
167
258
  }, [apiLocaleTag, firstMarket, productPageCampaignSlug, dealerSearchOptions, locationQuery]);
168
259
  useEffect(() => {
169
- if ((type === 'productPage' || type === 'findDealerDrawer') && !isOpen)
260
+ if ((type === 'productPage' || type === 'findDealerDrawer') && !isOpen && !urlDealerId)
170
261
  return;
171
262
  if (auth != null)
172
263
  return;
@@ -183,7 +274,7 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
183
274
  return () => {
184
275
  cancelled = true;
185
276
  };
186
- }, [auth, endpoint, isOpen, type]);
277
+ }, [auth, endpoint, isOpen, type, urlDealerId]);
187
278
  useEffect(() => {
188
279
  if (!shouldAutoLocateAfterAuth)
189
280
  return;
@@ -209,6 +300,21 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
209
300
  return 'typeReady';
210
301
  }, [type]);
211
302
  const widgetType = isWidgetType(type) ? type : undefined;
303
+ useEffect(() => {
304
+ if (widgetType !== 'findDealerDrawer' || typeof window === 'undefined')
305
+ return;
306
+ const syncFromUrl = () => {
307
+ setUrlDealerId(readDealerIdFromUrlSearch(window.location.search));
308
+ };
309
+ syncFromUrl();
310
+ window.addEventListener('popstate', syncFromUrl);
311
+ return () => {
312
+ window.removeEventListener('popstate', syncFromUrl);
313
+ };
314
+ }, [widgetType]);
315
+ useEffect(() => {
316
+ urlDealerHandledRef.current = false;
317
+ }, [urlDealerId]);
212
318
  useEffect(() => {
213
319
  if (!isOpen)
214
320
  return;
@@ -243,6 +349,11 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
243
349
  setLocationQuery('');
244
350
  setLocationSuggestions([]);
245
351
  setIsSuggestionListOpen(false);
352
+ setSelectedDealerId(null);
353
+ pendingDealerSearchTermRef.current = null;
354
+ tracker?.resetSession();
355
+ ensureDataLayer();
356
+ tracker?.trackWidgetOpened();
246
357
  setIsOpen(true);
247
358
  if (auth?.ok && auth.authorized === true && !isLoading) {
248
359
  setShouldAutoLocateAfterAuth(false);
@@ -251,24 +362,150 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
251
362
  else {
252
363
  setShouldAutoLocateAfterAuth(true);
253
364
  }
254
- }, [auth, isLoading, runLocationSearch]);
365
+ }, [auth, isLoading, runLocationSearch, tracker]);
255
366
  const closeDealerWidget = useCallback(() => {
256
367
  setIsOpen(false);
257
368
  setLocationSuggestions([]);
258
369
  setIsSuggestionListOpen(false);
259
370
  setIsSearchingSuggestions(false);
260
371
  setMapSearchResult(null);
261
- }, []);
372
+ setSelectedDealerId(null);
373
+ pendingDealerSearchTermRef.current = null;
374
+ tracker?.resetSession();
375
+ }, [tracker]);
262
376
  useImperativeHandle(widgetRef, () => ({
263
377
  open: openProductPageWidget,
264
378
  close: closeDealerWidget,
265
379
  isOpen,
266
380
  }), [openProductPageWidget, closeDealerWidget, isOpen]);
381
+ const handleInquiryClose = useCallback(() => {
382
+ setSelectedDealerName(null);
383
+ setSelectedDealerId(null);
384
+ setInquiryValues(null);
385
+ setInquiryError(null);
386
+ }, []);
387
+ const handleInquirySubmit = useCallback(async (turnstileToken) => {
388
+ if (inquiryValues == null || widgetFormId == null)
389
+ return;
390
+ const validationError = validateInquiryFormValues(inquiryValues);
391
+ if (validationError) {
392
+ setInquiryError(validationError.type === 'invalid_email'
393
+ ? t.formValidationEmail
394
+ : t.formValidationRequired);
395
+ tracker?.trackFormError(validationError);
396
+ return;
397
+ }
398
+ setInquiryError(null);
399
+ setIsSubmittingInquiry(true);
400
+ try {
401
+ const payload = inquiryFormValuesToApiPayload(inquiryValues, {
402
+ formId: widgetFormId,
403
+ categoryLabel: inquiryCategoryLabel(inquiryValues.requestCategory, t),
404
+ domain: typeof window !== 'undefined' ? window.location.hostname : undefined,
405
+ });
406
+ if (turnstileToken?.trim()) {
407
+ payload.turnstileToken = turnstileToken.trim();
408
+ }
409
+ const result = await submitInquiry(payload, { endpoint: submissionEndpoint });
410
+ if (result.ok !== true) {
411
+ setInquiryError(typeof result.error === 'string' ? result.error : t.genericWidgetError);
412
+ setTurnstileResetKey((current) => current + 1);
413
+ return;
414
+ }
415
+ setIsInquirySubmitted(true);
416
+ setSelectedDealerName(null);
417
+ setSelectedDealerId(null);
418
+ setInquiryValues(null);
419
+ setTurnstileResetKey((current) => current + 1);
420
+ tracker?.trackSuccessfulSubmit();
421
+ }
422
+ catch {
423
+ setInquiryError(t.genericWidgetError);
424
+ setTurnstileResetKey((current) => current + 1);
425
+ }
426
+ finally {
427
+ setIsSubmittingInquiry(false);
428
+ }
429
+ }, [
430
+ inquiryValues,
431
+ widgetFormId,
432
+ t,
433
+ tracker,
434
+ submissionEndpoint,
435
+ ]);
436
+ const handleStartInquiry = useCallback((dealer) => {
437
+ const dealerName = getDealerName(dealer, t.unknownDealer);
438
+ const dealerId = getDealerId(dealer);
439
+ setSelectedDealerName(dealerName);
440
+ setSelectedDealerId(dealerId ?? null);
441
+ setInquiryValues(createInquiryFormValues(productName, dealer, t.unknownDealer, apiMarkets?.[0]));
442
+ setInquiryError(null);
443
+ setIsInquirySubmitted(false);
444
+ tracker?.trackFormStart(dealerId, dealerName);
445
+ }, [apiMarkets, productName, t.unknownDealer, tracker]);
446
+ const openDrawerFromUrlDealer = useCallback(async (dealerId) => {
447
+ setIsLoadingUrlDealer(true);
448
+ try {
449
+ const result = await fetchDealerById(dealerId, dealerSearchOptions);
450
+ if (result.ok !== true || result.dealer == null) {
451
+ return;
452
+ }
453
+ setShouldAutoLocateAfterAuth(false);
454
+ setLocationError(null);
455
+ setSearchResult(null);
456
+ setMapSearchResult(null);
457
+ setLocationQuery('');
458
+ setLocationSuggestions([]);
459
+ setIsSuggestionListOpen(false);
460
+ pendingDealerSearchTermRef.current = null;
461
+ tracker?.resetSession();
462
+ ensureDataLayer();
463
+ tracker?.trackWidgetOpened();
464
+ const dealer = result.dealer;
465
+ const latitude = typeof dealer.latitude === 'number' && Number.isFinite(dealer.latitude)
466
+ ? dealer.latitude
467
+ : undefined;
468
+ const longitude = typeof dealer.longitude === 'number' && Number.isFinite(dealer.longitude)
469
+ ? dealer.longitude
470
+ : undefined;
471
+ const singleResult = {
472
+ ok: true,
473
+ type: 'geolocation',
474
+ total: 1,
475
+ dealers: [dealer],
476
+ ...(latitude != null && longitude != null
477
+ ? { origin: { latitude, longitude } }
478
+ : {}),
479
+ };
480
+ setSearchResult(singleResult);
481
+ setMapSearchResult(singleResult);
482
+ setIsOpen(true);
483
+ handleStartInquiry(dealer);
484
+ }
485
+ finally {
486
+ setIsLoadingUrlDealer(false);
487
+ }
488
+ }, [dealerSearchOptions, handleStartInquiry, tracker]);
489
+ useEffect(() => {
490
+ if (widgetType !== 'findDealerDrawer' || urlDealerId == null)
491
+ return;
492
+ if (auth == null || isLoading)
493
+ return;
494
+ if (!auth.ok || !auth.authorized)
495
+ return;
496
+ if (urlDealerHandledRef.current)
497
+ return;
498
+ urlDealerHandledRef.current = true;
499
+ void openDrawerFromUrlDealer(urlDealerId);
500
+ }, [auth, isLoading, openDrawerFromUrlDealer, urlDealerId, widgetType]);
267
501
  const shellClass = 'jwi-box-border jwi-flex jwi-w-[540px] jwi-max-w-full jwi-flex-col jwi-font-sans jwi-text-[#111111]';
268
502
  const rootClass = className != null && className !== '' ? `${shellClass} ${className}` : shellClass;
269
503
  if (typeState !== 'typeReady') {
270
504
  return _jsx("div", { className: rootClass, children: t.invalidWidgetTypeError });
271
505
  }
506
+ if (widgetType === 'warrantyForm') {
507
+ return (_jsx(WarrantyFormWidget, { endpoint: warrantyEndpoint, className: className, locale: localeProp, market: marketProp, styling: styling, turnstileSiteKey: turnstileSiteKeyProp, turnstileConfigEndpoint: turnstileConfigEndpoint }));
508
+ }
272
509
  if (widgetType === 'productPage' && !isOpen) {
273
510
  if (button == null)
274
511
  return null;
@@ -278,7 +515,7 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
278
515
  isOpen &&
279
516
  (auth === null || isLoading);
280
517
  if (productPageAuthPending) {
281
- return buttonLoading ?? button ?? null;
518
+ return resolveButtonLoading(button, buttonLoading, t.loading);
282
519
  }
283
520
  const waitingForAuth = auth === null &&
284
521
  !((widgetType === 'productPage' || widgetType === 'findDealerDrawer') &&
@@ -288,7 +525,13 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
288
525
  type !== 'findDealerDrawer') {
289
526
  return (_jsx("div", { className: rootClass, children: _jsx("div", { className: "jwi-flex jwi-w-full jwi-flex-col", children: _jsx(FinderSearchRowSkeleton, {}) }) }));
290
527
  }
291
- const deferAuthErrorUntilDrawerOpens = widgetType === 'findDealerDrawer' && !isOpen && auth === null;
528
+ const deferAuthErrorUntilDrawerOpens = widgetType === 'findDealerDrawer' && !isOpen && auth === null && urlDealerId == null;
529
+ if (urlDealerId != null &&
530
+ auth != null &&
531
+ !isLoading &&
532
+ (!auth.ok || !auth.authorized)) {
533
+ return _jsx("div", { className: rootClass, children: getSafeWidgetErrorMessage(auth?.error, t) });
534
+ }
292
535
  const deferAuthUntilFindDealerDrawerResolves = widgetType === 'findDealerDrawer' && isOpen && (auth === null || isLoading);
293
536
  if (!deferAuthErrorUntilDrawerOpens &&
294
537
  !deferAuthUntilFindDealerDrawerResolves &&
@@ -296,77 +539,50 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
296
539
  return _jsx("div", { className: rootClass, children: getSafeWidgetErrorMessage(auth?.error, t) });
297
540
  }
298
541
  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) {
542
+ return ProductPageWidgetComp != null && mounted && tracker != null
543
+ ? 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
544
+ ? { ...searchResult, error: getSafeWidgetErrorMessage(searchResult.error, t) }
545
+ : searchResult, mapSearchResult: mapSearchResult?.ok === false
546
+ ? { ...mapSearchResult, error: getSafeWidgetErrorMessage(mapSearchResult.error, t) }
547
+ : 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) => {
548
+ setLocationQuery(value);
549
+ const trimmed = value.trim();
550
+ setIsSuggestionListOpen(trimmed.length > 0);
551
+ if (trimmed.length < 3) {
552
+ setLocationSuggestions([]);
553
+ }
554
+ }, onQuerySubmit: async (value) => {
555
+ const query = value.trim();
556
+ if (query.length < 3)
557
+ return;
558
+ setIsSearchingSuggestions(true);
559
+ const result = await searchLocationSuggestions(query, dealerSearchOptions);
560
+ const resolvedSuggestions = result.ok && Array.isArray(result.suggestions) ? result.suggestions : [];
561
+ setLocationSuggestions(resolvedSuggestions);
562
+ setIsSearchingSuggestions(false);
563
+ const suggestion = resolvedSuggestions[0];
564
+ if (!suggestion)
565
+ return;
566
+ setLocationQuery(suggestion.label);
567
+ setLocationSuggestions([]);
568
+ setIsSearchingSuggestions(false);
569
+ setIsSuggestionListOpen(false);
570
+ await runUserDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude, query);
571
+ }, onSuggestionSelect: (suggestion) => {
572
+ setLocationQuery(suggestion.label);
309
573
  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)
574
+ setIsSearchingSuggestions(false);
575
+ setIsSuggestionListOpen(false);
576
+ void runUserDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude, suggestion.label);
577
+ }, onDismissSuggestions: () => {
578
+ setLocationSuggestions([]);
579
+ setIsSearchingSuggestions(false);
580
+ setIsSuggestionListOpen(false);
581
+ }, onInquiryClose: handleInquiryClose, onInquirySubmit: handleInquirySubmit, onInquiryFieldChange: (key, value) => setInquiryValues((current) => current == null ? current : { ...current, [key]: value }), onStartInquiry: handleStartInquiry, onMapDealerSelect: (dealer) => {
582
+ if (isDealerInSearchResult(dealer.dealerName, searchResult, t.unknownDealer))
583
+ return;
584
+ void runUserDealerSearchByCoordinates(dealer.latitude, dealer.longitude, locationQueryRef.current.trim());
585
+ }, onClosePopup: closeDealerWidget }) }), document.body)
370
586
  : null;
371
587
  }
372
588
  if (widgetType === 'findDealerDrawer') {
@@ -374,10 +590,11 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
374
590
  (auth === null ||
375
591
  isLoading ||
376
592
  isComponentLoading ||
377
- (isSearching && searchResult == null && mapSearchResult == null));
593
+ isLoadingUrlDealer ||
594
+ (isSearching && searchResult == null && mapSearchResult == null && urlDealerId == null));
378
595
  return (_jsxs(_Fragment, { children: [button != null &&
379
596
  (drawerLoading
380
- ? buttonLoading ?? button
597
+ ? resolveButtonLoading(button, buttonLoading, t.loading)
381
598
  : renderButton(button, openProductPageWidget)), mounted &&
382
599
  createPortal(_jsxs(_Fragment, { children: [_jsx("div", { className: "jwi-fixed jwi-inset-0 jwi-z-[2147483647] jwi-bg-black/35", style: {
383
600
  opacity: isOpen ? 1 : 0,
@@ -387,76 +604,49 @@ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, p
387
604
  transform: isOpen ? 'translateX(0)' : 'translateX(100%)',
388
605
  transition: 'transform 300ms ease-out',
389
606
  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) {
607
+ }, "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
608
+ ? { ...searchResult, error: getSafeWidgetErrorMessage(searchResult.error, t) }
609
+ : searchResult, mapSearchResult: mapSearchResult?.ok === false
610
+ ? { ...mapSearchResult, error: getSafeWidgetErrorMessage(mapSearchResult.error, t) }
611
+ : 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) => {
612
+ setLocationQuery(value);
613
+ const trimmed = value.trim();
614
+ setIsSuggestionListOpen(trimmed.length > 0);
615
+ if (trimmed.length < 3) {
616
+ setLocationSuggestions([]);
617
+ }
618
+ }, onQuerySubmit: async (value) => {
619
+ const query = value.trim();
620
+ if (query.length < 3)
621
+ return;
622
+ setIsSearchingSuggestions(true);
623
+ const result = await searchLocationSuggestions(query, dealerSearchOptions);
624
+ const resolvedSuggestions = result.ok && Array.isArray(result.suggestions) ? result.suggestions : [];
625
+ setLocationSuggestions(resolvedSuggestions);
626
+ setIsSearchingSuggestions(false);
627
+ const suggestion = resolvedSuggestions[0];
628
+ if (!suggestion)
629
+ return;
630
+ setLocationQuery(suggestion.label);
631
+ setLocationSuggestions([]);
632
+ setIsSearchingSuggestions(false);
633
+ setIsSuggestionListOpen(false);
634
+ await runUserDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude, query);
635
+ }, onSuggestionSelect: (suggestion) => {
636
+ setLocationQuery(suggestion.label);
637
+ setLocationSuggestions([]);
638
+ setIsSearchingSuggestions(false);
639
+ setIsSuggestionListOpen(false);
640
+ void runUserDealerSearchByCoordinates(suggestion.latitude, suggestion.longitude, suggestion.label);
641
+ }, onDismissSuggestions: () => {
399
642
  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)] }));
643
+ setIsSearchingSuggestions(false);
644
+ setIsSuggestionListOpen(false);
645
+ }, onInquiryClose: handleInquiryClose, onInquirySubmit: handleInquirySubmit, onInquiryFieldChange: (key, value) => setInquiryValues((current) => current == null ? current : { ...current, [key]: value }), onStartInquiry: handleStartInquiry, onMapDealerSelect: (dealer) => {
646
+ if (isDealerInSearchResult(dealer.dealerName, searchResult, t.unknownDealer))
647
+ return;
648
+ void runUserDealerSearchByCoordinates(dealer.latitude, dealer.longitude, locationQueryRef.current.trim());
649
+ }, onClose: closeDealerWidget }) })) : null) })] }), document.body)] }));
460
650
  }
461
651
  return _jsx("div", { className: rootClass, children: renderReadyState(widgetType, t) });
462
652
  }