sovads-sdk 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,31 +5,58 @@ class SovAds {
5
5
  this.components = new Map();
6
6
  this.siteId = null;
7
7
  this.renderObservers = new Map();
8
- this.debugLoggingEnabled = true;
8
+ this.debugLoggingEnabled = false;
9
+ this.adTrackingTokens = new Map();
9
10
  this.config = {
10
11
  apiUrl: typeof window !== 'undefined' && window.location.hostname === 'localhost'
11
12
  ? 'http://localhost:3000'
12
13
  : 'https://ads.sovseas.xyz',
13
14
  debug: false,
15
+ refreshInterval: 0, // No auto-refresh by default
16
+ lazyLoad: true,
17
+ rotationEnabled: true,
18
+ popupMinIntervalMinutes: 30,
19
+ popupSessionMax: 1,
14
20
  ...config
15
21
  };
22
+ this.debugLoggingEnabled = Boolean(this.config.debug);
16
23
  this.fingerprint = this.generateFingerprint();
17
24
  if (this.config.debug) {
18
25
  console.log('SovAds SDK initialized:', this.config);
19
26
  }
20
27
  }
21
28
  generateFingerprint() {
22
- const canvas = document.createElement('canvas');
23
- const ctx = canvas.getContext('2d');
24
- ctx?.fillText('SovAds fingerprint', 10, 10);
25
- const fingerprint = [
26
- navigator.userAgent,
27
- navigator.language,
28
- screen.width + 'x' + screen.height,
29
- new Date().getTimezoneOffset(),
30
- canvas.toDataURL()
31
- ].join('|');
32
- return btoa(fingerprint).substring(0, 16);
29
+ const storageKey = 'sovads_fingerprint_v1';
30
+ try {
31
+ if (typeof window !== 'undefined' && window.localStorage) {
32
+ const existing = window.localStorage.getItem(storageKey);
33
+ if (existing) {
34
+ return existing;
35
+ }
36
+ }
37
+ }
38
+ catch {
39
+ // Ignore storage access errors and fall back to generated value.
40
+ }
41
+ const browserParts = [
42
+ typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown-ua',
43
+ typeof navigator !== 'undefined' ? navigator.language : 'unknown-lang',
44
+ typeof screen !== 'undefined' ? `${screen.width}x${screen.height}` : 'unknown-screen',
45
+ String(new Date().getTimezoneOffset()),
46
+ typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
47
+ ? crypto.randomUUID()
48
+ : `${Date.now()}-${Math.random()}`,
49
+ ];
50
+ const value = btoa(browserParts.join('|')).replace(/=+$/g, '');
51
+ try {
52
+ if (typeof window !== 'undefined' && window.localStorage) {
53
+ window.localStorage.setItem(storageKey, value);
54
+ }
55
+ }
56
+ catch {
57
+ // Ignore storage write failures.
58
+ }
59
+ return value;
33
60
  }
34
61
  async detectSiteId() {
35
62
  if (this.siteId) {
@@ -47,6 +74,7 @@ class SovAds {
47
74
  const domain = window.location.hostname;
48
75
  const payload = {
49
76
  domain,
77
+ pathname: window.location.pathname,
50
78
  fingerprint: this.fingerprint,
51
79
  userAgent: navigator.userAgent,
52
80
  pageUrl: window.location.href,
@@ -216,13 +244,16 @@ class SovAds {
216
244
  * Normalize URL - add protocol if missing for localhost
217
245
  */
218
246
  normalizeUrl(url) {
219
- if (!url.includes('://')) {
247
+ const trimmed = url.trim();
248
+ if (!trimmed.includes('://')) {
220
249
  // Allow localhost URLs without protocol for debugging
221
- if (url.startsWith('localhost') || url.startsWith('127.0.0.1')) {
222
- return `http://${url}`;
250
+ if (trimmed.startsWith('localhost') || trimmed.startsWith('127.0.0.1')) {
251
+ return `http://${trimmed}`;
223
252
  }
253
+ // Treat bare domains as https by default.
254
+ return `https://${trimmed}`;
224
255
  }
225
- return url;
256
+ return trimmed;
226
257
  }
227
258
  /**
228
259
  * Validate URL format
@@ -237,6 +268,11 @@ class SovAds {
237
268
  return false;
238
269
  }
239
270
  }
271
+ inferMediaTypeFromUrl(url) {
272
+ const value = (url || '').toLowerCase();
273
+ const videoExts = ['.mp4', '.webm', '.ogg', '.ogv', '.mov', '.m3u8'];
274
+ return videoExts.some((ext) => value.includes(ext)) ? 'video' : 'image';
275
+ }
240
276
  /**
241
277
  * Fetch with retry logic
242
278
  */
@@ -263,13 +299,15 @@ class SovAds {
263
299
  }
264
300
  throw lastError || new Error('Fetch failed after retries');
265
301
  }
266
- async loadAd(consumerId) {
302
+ async loadAd(options = {}) {
267
303
  const startTime = Date.now();
268
304
  try {
269
305
  const siteId = await this.detectSiteId();
270
306
  const params = new URLSearchParams({
271
307
  siteId,
272
- ...(consumerId && { consumerId })
308
+ ...(options.consumerId && { consumerId: options.consumerId }),
309
+ ...(options.placement && { placement: options.placement }),
310
+ ...(options.size && { size: options.size }),
273
311
  });
274
312
  const endpoint = `${this.config.apiUrl}/api/ads?${params}`;
275
313
  const response = await this.fetchWithRetry(endpoint);
@@ -284,7 +322,7 @@ class SovAds {
284
322
  pageUrl: window.location.href,
285
323
  userAgent: navigator.userAgent,
286
324
  fingerprint: this.fingerprint,
287
- requestBody: { siteId, consumerId },
325
+ requestBody: { siteId, ...options },
288
326
  responseStatus: response.status,
289
327
  duration,
290
328
  });
@@ -335,8 +373,13 @@ class SovAds {
335
373
  ...rawAd,
336
374
  bannerUrl: this.normalizeUrl(rawAd.bannerUrl),
337
375
  targetUrl: this.normalizeUrl(rawAd.targetUrl),
338
- mediaType: rawAd.mediaType === 'video' ? 'video' : 'image',
376
+ mediaType: rawAd.mediaType === 'video'
377
+ ? 'video'
378
+ : this.inferMediaTypeFromUrl(this.normalizeUrl(rawAd.bannerUrl)),
339
379
  };
380
+ if (normalizedAd.trackingToken) {
381
+ this.adTrackingTokens.set(normalizedAd.id, normalizedAd.trackingToken);
382
+ }
340
383
  if (this.config.debug) {
341
384
  console.log('Ad loaded:', normalizedAd);
342
385
  }
@@ -370,6 +413,105 @@ class SovAds {
370
413
  return null;
371
414
  }
372
415
  }
416
+ toBase64(bytes) {
417
+ let binary = '';
418
+ for (const b of bytes) {
419
+ binary += String.fromCharCode(b);
420
+ }
421
+ return btoa(binary);
422
+ }
423
+ async signTrackingPayload(payload, timestamp) {
424
+ if (!this.config.apiSecret || typeof crypto === 'undefined' || !crypto.subtle) {
425
+ return null;
426
+ }
427
+ try {
428
+ const encoder = new TextEncoder();
429
+ const key = await crypto.subtle.importKey('raw', encoder.encode(this.config.apiSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
430
+ const message = `${timestamp}:${payload}`;
431
+ const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(message));
432
+ return this.toBase64(new Uint8Array(signature));
433
+ }
434
+ catch (error) {
435
+ if (this.config.debug) {
436
+ console.error('Failed to sign tracking payload:', error);
437
+ }
438
+ return null;
439
+ }
440
+ }
441
+ async sendTrackingEnvelope(eventPayload, useBeacon) {
442
+ if (eventPayload.trackingToken) {
443
+ const tokenBody = JSON.stringify({
444
+ trackingToken: eventPayload.trackingToken,
445
+ payload: eventPayload,
446
+ });
447
+ const tokenWebhookUrl = `${this.config.apiUrl}/api/webhook/track`;
448
+ try {
449
+ if (useBeacon && navigator.sendBeacon) {
450
+ return navigator.sendBeacon(tokenWebhookUrl, new Blob([tokenBody], { type: 'application/json' }));
451
+ }
452
+ const response = await fetch(tokenWebhookUrl, {
453
+ method: 'POST',
454
+ headers: { 'Content-Type': 'application/json' },
455
+ body: tokenBody,
456
+ keepalive: true,
457
+ });
458
+ return response.ok;
459
+ }
460
+ catch {
461
+ return false;
462
+ }
463
+ }
464
+ if (!this.config.apiKey || !this.config.apiSecret) {
465
+ if (this.config.debug) {
466
+ const devWebhookUrl = `${this.config.apiUrl}/api/webhook/beacon`;
467
+ const body = JSON.stringify(eventPayload);
468
+ try {
469
+ if (useBeacon && navigator.sendBeacon) {
470
+ return navigator.sendBeacon(devWebhookUrl, new Blob([body], { type: 'application/json' }));
471
+ }
472
+ const response = await fetch(devWebhookUrl, {
473
+ method: 'POST',
474
+ headers: { 'Content-Type': 'application/json' },
475
+ body,
476
+ keepalive: true,
477
+ });
478
+ return response.ok;
479
+ }
480
+ catch {
481
+ return false;
482
+ }
483
+ }
484
+ else {
485
+ console.warn('SovAds: Missing apiKey/apiSecret, skipping signed tracking event');
486
+ }
487
+ return false;
488
+ }
489
+ const timestamp = Date.now();
490
+ const payload = JSON.stringify(eventPayload);
491
+ const signature = await this.signTrackingPayload(payload, timestamp);
492
+ if (!signature) {
493
+ return false;
494
+ }
495
+ const envelope = JSON.stringify({
496
+ apiKey: this.config.apiKey,
497
+ siteId: eventPayload.siteId,
498
+ payload,
499
+ signature,
500
+ timestamp,
501
+ });
502
+ const webhookUrl = `${this.config.apiUrl}/api/webhook/track`;
503
+ if (useBeacon && navigator.sendBeacon) {
504
+ const blob = new Blob([envelope], { type: 'application/json' });
505
+ return navigator.sendBeacon(webhookUrl, blob);
506
+ }
507
+ const response = await fetch(webhookUrl, {
508
+ method: 'POST',
509
+ headers: { 'Content-Type': 'application/json' },
510
+ body: envelope,
511
+ keepalive: true,
512
+ });
513
+ return response.ok;
514
+ }
373
515
  /**
374
516
  * Track event with retry logic (internal helper)
375
517
  */
@@ -389,24 +531,15 @@ class SovAds {
389
531
  renderTime: renderInfo?.renderTime ?? Date.now(),
390
532
  timestamp: metadata.timestamp,
391
533
  pageUrl: metadata.pageUrl,
392
- userAgent: metadata.userAgent
534
+ userAgent: metadata.userAgent,
535
+ trackingToken: this.adTrackingTokens.get(adId),
393
536
  };
394
- const webhookUrl = `${this.config.apiUrl}/api/webhook/beacon`;
395
- const response = await fetch(webhookUrl, {
396
- method: 'POST',
397
- headers: {
398
- 'Content-Type': 'application/json'
399
- },
400
- body: JSON.stringify(payload),
401
- keepalive: true // Similar behavior to beacon
402
- });
403
- if (response.ok) {
404
- if (this.config.debug) {
405
- console.log(`SovAds: Tracked ${type} event via fetch (attempt ${attempt})`, payload);
406
- }
537
+ const ok = await this.sendTrackingEnvelope(payload, false);
538
+ if (!ok) {
539
+ throw new Error('Tracking endpoint rejected event');
407
540
  }
408
- else {
409
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
541
+ if (this.config.debug) {
542
+ console.log(`SovAds: Tracked ${type} event via signed fetch (attempt ${attempt})`, payload);
410
543
  }
411
544
  }
412
545
  catch (error) {
@@ -446,35 +579,25 @@ class SovAds {
446
579
  renderTime: renderInfo?.renderTime ?? Date.now(),
447
580
  timestamp: metadata.timestamp,
448
581
  pageUrl: metadata.pageUrl,
449
- userAgent: metadata.userAgent
582
+ userAgent: metadata.userAgent,
583
+ trackingToken: this.adTrackingTokens.get(adId),
450
584
  };
451
- // Use sendBeacon for reliable delivery (better for tracking impressions)
452
- // Beacon API ensures events are sent even if user navigates away
453
- if (navigator.sendBeacon) {
454
- const blob = new Blob([JSON.stringify(payload)], {
455
- type: 'application/json'
456
- });
457
- // Send to dedicated webhook endpoint for beamer interactions
458
- const webhookUrl = `${this.config.apiUrl}/api/webhook/beacon`;
459
- const sent = navigator.sendBeacon(webhookUrl, blob);
460
- if (!sent) {
461
- // Beacon failed, fallback to fetch with retry
585
+ if (typeof navigator.sendBeacon === 'function') {
586
+ const sent = await this.sendTrackingEnvelope(payload, true);
587
+ if (sent) {
462
588
  if (this.config.debug) {
463
- console.warn(`SovAds: Beacon failed for ${type}, falling back to fetch`);
589
+ console.log(`SovAds: Tracked ${type} event via signed beacon`, {
590
+ payload: { ...payload, fingerprint: payload.fingerprint.substring(0, 8) + '...' }
591
+ });
464
592
  }
465
- await this.trackEventWithRetry(type, adId, campaignId, renderInfo, 1);
466
- }
467
- else if (this.config.debug) {
468
- console.log(`SovAds: Tracked ${type} event via beacon`, {
469
- sent,
470
- payload: { ...payload, fingerprint: payload.fingerprint.substring(0, 8) + '...' }
471
- });
593
+ return;
472
594
  }
473
595
  }
474
- else {
475
- // Fallback to fetch for older browsers
476
- await this.trackEventWithRetry(type, adId, campaignId, renderInfo, 1);
596
+ // Fallback to signed fetch for older browsers and beacon failures
597
+ if (this.config.debug) {
598
+ console.warn(`SovAds: Beacon unavailable/failed for ${type}, falling back to signed fetch`);
477
599
  }
600
+ await this.trackEventWithRetry(type, adId, campaignId, renderInfo, 1);
478
601
  }
479
602
  catch (error) {
480
603
  if (this.config.debug) {
@@ -555,17 +678,22 @@ class SovAds {
555
678
  }
556
679
  // Banner Component
557
680
  export class Banner {
558
- constructor(sovads, containerId) {
681
+ constructor(sovads, containerId, slotConfig = {}) {
559
682
  this.currentAd = null;
560
683
  this.renderStartTime = 0;
561
684
  this.hasTrackedImpression = false;
562
685
  this.isRendering = false;
686
+ this.refreshTimer = null;
687
+ this.lastAdId = null;
688
+ this.retryCount = 0;
689
+ this.maxRetries = 3;
563
690
  this.sovads = sovads;
564
691
  this.containerId = containerId;
692
+ this.slotConfig = slotConfig;
565
693
  }
566
- async render(consumerId) {
694
+ async render(consumerId, forceRefresh = false) {
567
695
  // Prevent concurrent renders
568
- if (this.isRendering) {
696
+ if (this.isRendering && !forceRefresh) {
569
697
  if (this.sovads.getConfig().debug) {
570
698
  console.warn(`Banner render already in progress for ${this.containerId}`);
571
699
  }
@@ -579,8 +707,33 @@ export class Banner {
579
707
  this.isRendering = false;
580
708
  return;
581
709
  }
710
+ // Lazy loading: wait for container to be in viewport
711
+ if (this.sovads.getConfig().lazyLoad && !forceRefresh) {
712
+ const isInViewport = await this.checkViewport(container);
713
+ if (!isInViewport) {
714
+ // Set up intersection observer for lazy loading
715
+ this.setupLazyLoadObserver(container, consumerId);
716
+ this.isRendering = false;
717
+ return;
718
+ }
719
+ }
582
720
  this.renderStartTime = Date.now();
583
- this.currentAd = await this.sovads.loadAd(consumerId);
721
+ this.currentAd = await this.sovads.loadAd({
722
+ consumerId,
723
+ placement: this.slotConfig.placementId || 'banner',
724
+ size: this.slotConfig.size,
725
+ });
726
+ this.hasTrackedImpression = false;
727
+ // Skip if same ad (rotation disabled or same ad returned)
728
+ if (!forceRefresh && this.lastAdId === this.currentAd?.id && this.sovads.getConfig().rotationEnabled) {
729
+ if (this.sovads.getConfig().debug) {
730
+ console.log('Same ad returned, skipping render');
731
+ }
732
+ this.isRendering = false;
733
+ return;
734
+ }
735
+ this.lastAdId = this.currentAd?.id || null;
736
+ this.retryCount = 0; // Reset retry count on success
584
737
  if (!this.currentAd) {
585
738
  container.innerHTML = '<div class="sovads-no-ad">No ads available</div>';
586
739
  this.isRendering = false;
@@ -588,6 +741,7 @@ export class Banner {
588
741
  }
589
742
  // Handle dummy ads for unregistered sites
590
743
  if (this.currentAd.isDummy) {
744
+ container.innerHTML = '';
591
745
  const dummyElement = document.createElement('div');
592
746
  dummyElement.className = 'sovads-banner-dummy';
593
747
  dummyElement.setAttribute('data-ad-id', this.currentAd.id);
@@ -633,16 +787,21 @@ export class Banner {
633
787
  return;
634
788
  }
635
789
  const adElement = document.createElement('div');
790
+ container.innerHTML = '';
636
791
  adElement.className = 'sovads-banner';
637
792
  adElement.setAttribute('data-ad-id', this.currentAd.id);
793
+ const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
638
794
  adElement.style.cssText = `
639
795
  border: 1px solid #333;
640
796
  border-radius: 8px;
641
797
  overflow: hidden;
642
- cursor: pointer;
798
+ cursor: ${mediaType === 'video' ? 'default' : 'pointer'};
643
799
  transition: transform 0.2s ease;
800
+ max-width: 100%;
801
+ width: 100%;
802
+ box-sizing: border-box;
803
+ opacity: 0;
644
804
  `;
645
- const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
646
805
  const handleVisibilityTracking = (renderInfo) => {
647
806
  this.sovads.setupRenderObserver(adElement, this.currentAd.id, (isVisible) => {
648
807
  renderInfo.viewportVisible = isVisible;
@@ -653,6 +812,7 @@ export class Banner {
653
812
  });
654
813
  };
655
814
  const handleRenderSuccess = () => {
815
+ adElement.style.opacity = '1';
656
816
  const renderTime = Date.now() - this.renderStartTime;
657
817
  handleVisibilityTracking({
658
818
  rendered: true,
@@ -661,6 +821,7 @@ export class Banner {
661
821
  });
662
822
  };
663
823
  const handleRenderError = () => {
824
+ adElement.style.opacity = '1';
664
825
  if (this.sovads.getConfig().debug) {
665
826
  console.warn(`Failed to load ad media: ${this.currentAd.bannerUrl}`);
666
827
  }
@@ -688,20 +849,19 @@ export class Banner {
688
849
  const img = document.createElement('img');
689
850
  img.src = this.currentAd.bannerUrl;
690
851
  img.alt = this.currentAd.description;
691
- img.style.cssText = 'width: 100%; height: auto; display: block;';
852
+ img.style.cssText = 'width: 100%; height: auto; display: block; max-width: 100%; object-fit: contain;';
692
853
  img.addEventListener('load', handleRenderSuccess, { once: true });
693
854
  img.addEventListener('error', handleRenderError, { once: true });
694
855
  mediaElement = img;
695
856
  }
696
- mediaElement.style.cursor = 'pointer';
697
- // Add click handler
698
- adElement.addEventListener('click', () => {
857
+ mediaElement.style.cursor = mediaType === 'video' ? 'default' : 'pointer';
858
+ mediaElement.style.maxWidth = '100%';
859
+ const handleClickThrough = () => {
699
860
  this.sovads._trackEvent('CLICK', this.currentAd.id, this.currentAd.campaignId, {
700
861
  rendered: true,
701
862
  viewportVisible: true,
702
863
  renderTime: Date.now() - this.renderStartTime
703
864
  });
704
- // Log interaction
705
865
  this.sovads.logInteraction('CLICK', {
706
866
  adId: this.currentAd.id,
707
867
  campaignId: this.currentAd.campaignId,
@@ -709,7 +869,30 @@ export class Banner {
709
869
  metadata: { renderTime: Date.now() - this.renderStartTime },
710
870
  });
711
871
  window.open(this.sovads.normalizeUrl(this.currentAd.targetUrl), '_blank', 'noopener,noreferrer');
712
- });
872
+ };
873
+ if (mediaType === 'video') {
874
+ const ctaButton = document.createElement('button');
875
+ ctaButton.type = 'button';
876
+ ctaButton.textContent = 'Learn more';
877
+ ctaButton.style.cssText = `
878
+ width: 100%;
879
+ border: none;
880
+ border-top: 1px solid #333;
881
+ background: #111;
882
+ color: #fff;
883
+ font-size: 12px;
884
+ font-weight: 600;
885
+ padding: 8px 12px;
886
+ cursor: pointer;
887
+ `;
888
+ ctaButton.addEventListener('click', handleClickThrough);
889
+ adElement.appendChild(mediaElement);
890
+ adElement.appendChild(ctaButton);
891
+ }
892
+ else {
893
+ adElement.addEventListener('click', handleClickThrough);
894
+ adElement.appendChild(mediaElement);
895
+ }
713
896
  // Add hover effect
714
897
  adElement.addEventListener('mouseenter', () => {
715
898
  adElement.style.transform = 'scale(1.02)';
@@ -717,13 +900,94 @@ export class Banner {
717
900
  adElement.addEventListener('mouseleave', () => {
718
901
  adElement.style.transform = 'scale(1)';
719
902
  });
720
- adElement.appendChild(mediaElement);
721
903
  container.appendChild(adElement);
904
+ // Set up auto-refresh if enabled
905
+ this.setupAutoRefresh(consumerId);
906
+ }
907
+ catch (error) {
908
+ // Retry logic on error
909
+ if (this.retryCount < this.maxRetries) {
910
+ this.retryCount++;
911
+ if (this.sovads.getConfig().debug) {
912
+ console.warn(`Banner render failed, retrying (${this.retryCount}/${this.maxRetries})...`);
913
+ }
914
+ await new Promise(resolve => setTimeout(resolve, 1000 * this.retryCount)); // Exponential backoff
915
+ this.isRendering = false;
916
+ return this.render(consumerId, true);
917
+ }
918
+ else {
919
+ const container = document.getElementById(this.containerId);
920
+ if (container) {
921
+ container.innerHTML = '<div class="sovads-error" style="padding: 10px; text-align: center; color: #666; font-size: 12px;">Ad temporarily unavailable</div>';
922
+ }
923
+ if (this.sovads.getConfig().debug) {
924
+ console.error('Banner render failed after retries:', error);
925
+ }
926
+ }
722
927
  }
723
928
  finally {
724
929
  this.isRendering = false;
725
930
  }
726
931
  }
932
+ async checkViewport(element) {
933
+ return new Promise((resolve) => {
934
+ if (typeof IntersectionObserver === 'undefined') {
935
+ resolve(true); // Fallback: load immediately if IntersectionObserver not supported
936
+ return;
937
+ }
938
+ const observer = new IntersectionObserver((entries) => {
939
+ entries.forEach((entry) => {
940
+ if (entry.isIntersecting) {
941
+ observer.disconnect();
942
+ resolve(true);
943
+ }
944
+ });
945
+ }, { rootMargin: '50px' } // Start loading 50px before entering viewport
946
+ );
947
+ observer.observe(element);
948
+ // Timeout after 5 seconds - load anyway
949
+ setTimeout(() => {
950
+ observer.disconnect();
951
+ resolve(true);
952
+ }, 5000);
953
+ });
954
+ }
955
+ setupLazyLoadObserver(container, consumerId) {
956
+ if (typeof IntersectionObserver === 'undefined') {
957
+ // Fallback: load immediately
958
+ this.render(consumerId);
959
+ return;
960
+ }
961
+ const observer = new IntersectionObserver((entries) => {
962
+ entries.forEach((entry) => {
963
+ if (entry.isIntersecting && !this.isRendering) {
964
+ observer.disconnect();
965
+ this.render(consumerId);
966
+ }
967
+ });
968
+ }, { rootMargin: '50px' });
969
+ observer.observe(container);
970
+ }
971
+ setupAutoRefresh(consumerId) {
972
+ // Clear existing timer
973
+ if (this.refreshTimer) {
974
+ clearInterval(this.refreshTimer);
975
+ }
976
+ const refreshInterval = this.sovads.getConfig().refreshInterval || 0;
977
+ if (refreshInterval > 0) {
978
+ this.refreshTimer = window.setInterval(() => {
979
+ if (!this.isRendering) {
980
+ this.render(consumerId, true);
981
+ }
982
+ }, refreshInterval * 1000);
983
+ }
984
+ }
985
+ destroy() {
986
+ if (this.refreshTimer) {
987
+ clearInterval(this.refreshTimer);
988
+ this.refreshTimer = null;
989
+ }
990
+ }
727
991
  }
728
992
  // Popup Component
729
993
  export class Popup {
@@ -731,8 +995,40 @@ export class Popup {
731
995
  this.currentAd = null;
732
996
  this.popupElement = null;
733
997
  this.isShowing = false;
998
+ this.retryCount = 0;
999
+ this.maxRetries = 3;
1000
+ this.storageKeyLastShown = 'sovads_popup_last_shown';
1001
+ this.storageKeySessionCount = 'sovads_popup_session_count';
734
1002
  this.sovads = sovads;
735
1003
  }
1004
+ canShowByFrequencyCap() {
1005
+ try {
1006
+ const minIntervalMs = (this.sovads.getConfig().popupMinIntervalMinutes || 30) * 60 * 1000;
1007
+ const sessionMax = this.sovads.getConfig().popupSessionMax || 1;
1008
+ const now = Date.now();
1009
+ const lastShown = Number(localStorage.getItem(this.storageKeyLastShown) || 0);
1010
+ const sessionCount = Number(sessionStorage.getItem(this.storageKeySessionCount) || 0);
1011
+ if (sessionCount >= sessionMax)
1012
+ return false;
1013
+ if (lastShown > 0 && now - lastShown < minIntervalMs)
1014
+ return false;
1015
+ return true;
1016
+ }
1017
+ catch {
1018
+ return true;
1019
+ }
1020
+ }
1021
+ markShown() {
1022
+ try {
1023
+ const now = Date.now();
1024
+ const currentSessionCount = Number(sessionStorage.getItem(this.storageKeySessionCount) || 0);
1025
+ localStorage.setItem(this.storageKeyLastShown, String(now));
1026
+ sessionStorage.setItem(this.storageKeySessionCount, String(currentSessionCount + 1));
1027
+ }
1028
+ catch {
1029
+ // Ignore storage access issues.
1030
+ }
1031
+ }
736
1032
  async show(consumerId, delay = 3000) {
737
1033
  // Prevent concurrent shows
738
1034
  if (this.isShowing) {
@@ -741,65 +1037,113 @@ export class Popup {
741
1037
  }
742
1038
  return;
743
1039
  }
744
- this.isShowing = true;
745
- this.currentAd = await this.sovads.loadAd(consumerId);
746
- if (!this.currentAd) {
747
- console.log('No popup ad available');
748
- this.isShowing = false;
1040
+ if (!this.canShowByFrequencyCap()) {
1041
+ if (this.sovads.getConfig().debug) {
1042
+ console.log('Popup skipped due to frequency cap');
1043
+ }
749
1044
  return;
750
1045
  }
751
- // Show popup after delay
752
- setTimeout(() => {
753
- this.renderPopup();
1046
+ this.isShowing = true;
1047
+ try {
1048
+ this.currentAd = await this.sovads.loadAd({
1049
+ consumerId,
1050
+ placement: 'popup',
1051
+ size: window.innerWidth < 640 ? '320x100' : '360x120',
1052
+ });
1053
+ if (!this.currentAd) {
1054
+ if (this.retryCount < this.maxRetries) {
1055
+ this.retryCount++;
1056
+ await new Promise(resolve => setTimeout(resolve, 1000 * this.retryCount));
1057
+ this.isShowing = false;
1058
+ return this.show(consumerId, delay);
1059
+ }
1060
+ if (this.sovads.getConfig().debug) {
1061
+ console.log('No popup ad available after retries');
1062
+ }
1063
+ this.isShowing = false;
1064
+ this.retryCount = 0;
1065
+ return;
1066
+ }
1067
+ this.retryCount = 0; // Reset on success
1068
+ // Show popup after delay
1069
+ setTimeout(() => {
1070
+ this.renderPopup();
1071
+ this.markShown();
1072
+ this.isShowing = false;
1073
+ }, delay);
1074
+ }
1075
+ catch (error) {
1076
+ if (this.sovads.getConfig().debug) {
1077
+ console.error('Error loading popup ad:', error);
1078
+ }
754
1079
  this.isShowing = false;
755
- }, delay);
1080
+ this.retryCount = 0;
1081
+ }
756
1082
  }
757
1083
  renderPopup() {
758
1084
  if (!this.currentAd)
759
1085
  return;
760
1086
  const renderStartTime = Date.now();
761
- // Don't track impressions for dummy ads
762
- if (!this.currentAd.isDummy) {
763
- // Track impression immediately when popup is rendered (not waiting for image load)
764
- // This ensures impression is tracked even if user closes popup quickly
1087
+ let impressionTracked = false;
1088
+ const trackPopupImpression = (rendered, renderTime) => {
1089
+ if (impressionTracked || !this.currentAd || this.currentAd.isDummy)
1090
+ return;
1091
+ impressionTracked = true;
765
1092
  this.sovads._trackEvent('IMPRESSION', this.currentAd.id, this.currentAd.campaignId, {
766
- rendered: true,
1093
+ rendered,
767
1094
  viewportVisible: true,
768
- renderTime: 0 // Will be updated when image loads
1095
+ renderTime,
769
1096
  });
770
- // Log interaction
771
1097
  this.sovads.logInteraction('IMPRESSION', {
772
1098
  adId: this.currentAd.id,
773
1099
  campaignId: this.currentAd.campaignId,
774
1100
  elementType: 'POPUP',
775
- metadata: { renderTime: 0 },
1101
+ metadata: { renderTime, rendered },
776
1102
  });
777
- }
778
- // Create overlay
779
- const overlay = document.createElement('div');
780
- overlay.className = 'sovads-popup-overlay';
781
- overlay.style.cssText = `
1103
+ };
1104
+ // Create non-blocking sticky container
1105
+ const wrapper = document.createElement('div');
1106
+ wrapper.className = 'sovads-popup-overlay';
1107
+ wrapper.style.cssText = `
782
1108
  position: fixed;
783
- top: 0;
784
- left: 0;
785
- width: 100%;
786
- height: 100%;
787
- background: rgba(0, 0, 0, 0.5);
1109
+ right: 16px;
1110
+ bottom: 16px;
1111
+ width: min(360px, calc(100vw - 24px));
788
1112
  z-index: 10000;
789
- display: flex;
790
- align-items: center;
791
- justify-content: center;
792
1113
  `;
793
1114
  // Create popup
794
1115
  this.popupElement = document.createElement('div');
795
1116
  this.popupElement.style.cssText = `
796
1117
  background: white;
797
1118
  border-radius: 12px;
798
- padding: 20px;
799
- max-width: 400px;
1119
+ padding: 14px;
1120
+ max-width: 360px;
800
1121
  position: relative;
801
1122
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
1123
+ opacity: 0;
1124
+ transition: opacity 0.2s ease;
1125
+ `;
1126
+ // SovAds logo badge in small left corner
1127
+ const logoBadge = document.createElement('div');
1128
+ logoBadge.style.cssText = `
1129
+ position: absolute;
1130
+ top: 8px;
1131
+ left: 12px;
1132
+ width: 24px;
1133
+ height: 24px;
1134
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1135
+ border-radius: 4px;
1136
+ display: flex;
1137
+ align-items: center;
1138
+ justify-content: center;
1139
+ font-size: 10px;
1140
+ font-weight: bold;
1141
+ color: white;
1142
+ z-index: 1;
1143
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
802
1144
  `;
1145
+ logoBadge.textContent = 'SA';
1146
+ logoBadge.title = 'SovAds';
803
1147
  // Close button
804
1148
  const closeBtn = document.createElement('button');
805
1149
  closeBtn.innerHTML = '×';
@@ -812,10 +1156,24 @@ export class Popup {
812
1156
  font-size: 24px;
813
1157
  cursor: pointer;
814
1158
  color: #666;
1159
+ z-index: 2;
815
1160
  `;
816
1161
  closeBtn.addEventListener('click', () => {
817
1162
  this.hide();
818
1163
  });
1164
+ // Add "Ad" message text below logo
1165
+ const adLabel = document.createElement('div');
1166
+ adLabel.style.cssText = `
1167
+ position: absolute;
1168
+ top: 36px;
1169
+ left: 12px;
1170
+ font-size: 9px;
1171
+ color: #999;
1172
+ font-weight: 500;
1173
+ text-transform: uppercase;
1174
+ letter-spacing: 0.5px;
1175
+ `;
1176
+ adLabel.textContent = 'Ad';
819
1177
  // Handle dummy ads
820
1178
  if (this.currentAd.isDummy) {
821
1179
  const dummyContent = document.createElement('div');
@@ -844,10 +1202,12 @@ export class Popup {
844
1202
  dummyContent.appendChild(img);
845
1203
  dummyContent.appendChild(message);
846
1204
  dummyContent.appendChild(link);
1205
+ this.popupElement.appendChild(logoBadge);
1206
+ this.popupElement.appendChild(adLabel);
847
1207
  this.popupElement.appendChild(closeBtn);
848
1208
  this.popupElement.appendChild(dummyContent);
849
- overlay.appendChild(this.popupElement);
850
- document.body.appendChild(overlay);
1209
+ wrapper.appendChild(this.popupElement);
1210
+ document.body.appendChild(wrapper);
851
1211
  // Auto close after 10 seconds
852
1212
  setTimeout(() => {
853
1213
  this.hide();
@@ -856,15 +1216,14 @@ export class Popup {
856
1216
  }
857
1217
  const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
858
1218
  const handleMediaError = () => {
1219
+ if (this.popupElement) {
1220
+ this.popupElement.style.opacity = '1';
1221
+ }
859
1222
  if (this.sovads.getConfig().debug) {
860
1223
  console.warn(`Failed to load popup ad media: ${this.currentAd.bannerUrl}`);
861
1224
  }
862
1225
  const renderTime = Date.now() - renderStartTime;
863
- this.sovads._trackEvent('IMPRESSION', this.currentAd.id, this.currentAd.campaignId, {
864
- rendered: false,
865
- viewportVisible: true,
866
- renderTime
867
- });
1226
+ trackPopupImpression(false, renderTime);
868
1227
  };
869
1228
  let mediaElement;
870
1229
  if (mediaType === 'video') {
@@ -877,7 +1236,11 @@ export class Popup {
877
1236
  video.controls = true;
878
1237
  video.style.cssText = 'width: 100%; height: auto; border-radius: 8px; cursor: pointer;';
879
1238
  video.addEventListener('loadeddata', () => {
1239
+ if (this.popupElement) {
1240
+ this.popupElement.style.opacity = '1';
1241
+ }
880
1242
  const renderTime = Date.now() - renderStartTime;
1243
+ trackPopupImpression(true, renderTime);
881
1244
  if (this.sovads.getConfig().debug) {
882
1245
  console.log(`Popup ad video loaded in ${renderTime}ms`);
883
1246
  }
@@ -891,7 +1254,11 @@ export class Popup {
891
1254
  img.alt = this.currentAd.description;
892
1255
  img.style.cssText = 'width: 100%; height: auto; border-radius: 8px; cursor: pointer;';
893
1256
  img.addEventListener('load', () => {
1257
+ if (this.popupElement) {
1258
+ this.popupElement.style.opacity = '1';
1259
+ }
894
1260
  const renderTime = Date.now() - renderStartTime;
1261
+ trackPopupImpression(true, renderTime);
895
1262
  if (this.sovads.getConfig().debug) {
896
1263
  console.log(`Popup ad image loaded in ${renderTime}ms`);
897
1264
  }
@@ -899,27 +1266,53 @@ export class Popup {
899
1266
  img.addEventListener('error', handleMediaError);
900
1267
  mediaElement = img;
901
1268
  }
902
- mediaElement.style.cursor = 'pointer';
903
- mediaElement.addEventListener('click', () => {
1269
+ const handleClickThrough = () => {
904
1270
  this.sovads._trackEvent('CLICK', this.currentAd.id, this.currentAd.campaignId, {
905
1271
  rendered: true,
906
1272
  viewportVisible: true,
907
1273
  renderTime: Date.now() - renderStartTime
908
1274
  });
909
- // Log interaction
910
1275
  this.sovads.logInteraction('CLICK', {
911
1276
  adId: this.currentAd.id,
912
1277
  campaignId: this.currentAd.campaignId,
913
1278
  elementType: 'POPUP',
914
1279
  metadata: { renderTime: Date.now() - renderStartTime },
915
1280
  });
916
- window.open(this.currentAd.targetUrl, '_blank', 'noopener,noreferrer');
1281
+ window.open(this.sovads.normalizeUrl(this.currentAd.targetUrl), '_blank', 'noopener,noreferrer');
917
1282
  this.hide();
918
- });
1283
+ };
1284
+ if (mediaType === 'video') {
1285
+ mediaElement.style.cursor = 'default';
1286
+ }
1287
+ else {
1288
+ mediaElement.style.cursor = 'pointer';
1289
+ mediaElement.addEventListener('click', handleClickThrough);
1290
+ }
1291
+ this.popupElement.appendChild(logoBadge);
1292
+ this.popupElement.appendChild(adLabel);
919
1293
  this.popupElement.appendChild(closeBtn);
920
1294
  this.popupElement.appendChild(mediaElement);
921
- overlay.appendChild(this.popupElement);
922
- document.body.appendChild(overlay);
1295
+ if (mediaType === 'video') {
1296
+ const ctaButton = document.createElement('button');
1297
+ ctaButton.type = 'button';
1298
+ ctaButton.textContent = 'Learn more';
1299
+ ctaButton.style.cssText = `
1300
+ width: 100%;
1301
+ margin-top: 10px;
1302
+ border: none;
1303
+ border-radius: 6px;
1304
+ background: #111;
1305
+ color: #fff;
1306
+ font-size: 12px;
1307
+ font-weight: 600;
1308
+ padding: 10px 12px;
1309
+ cursor: pointer;
1310
+ `;
1311
+ ctaButton.addEventListener('click', handleClickThrough);
1312
+ this.popupElement.appendChild(ctaButton);
1313
+ }
1314
+ wrapper.appendChild(this.popupElement);
1315
+ document.body.appendChild(wrapper);
923
1316
  // Auto close after 10 seconds
924
1317
  setTimeout(() => {
925
1318
  this.hide();
@@ -927,32 +1320,44 @@ export class Popup {
927
1320
  }
928
1321
  hide() {
929
1322
  const overlay = document.querySelector('.sovads-popup-overlay');
930
- if (overlay && overlay.isConnected) {
1323
+ if (overlay) {
931
1324
  try {
932
- overlay.remove();
1325
+ // Check if element is still connected to DOM before removing
1326
+ if (overlay.isConnected) {
1327
+ // Use remove() method which is safer and doesn't require parentNode
1328
+ overlay.remove();
1329
+ }
933
1330
  }
934
1331
  catch (error) {
935
1332
  // Element may have already been removed by React or another process
1333
+ // Silently fail - this is expected in some cases
936
1334
  if (this.sovads.getConfig().debug) {
937
1335
  console.warn('Could not remove popup overlay:', error);
938
1336
  }
939
1337
  }
940
1338
  }
1339
+ this.popupElement = null;
1340
+ this.currentAd = null;
941
1341
  }
942
1342
  }
943
1343
  // Sidebar Component
944
1344
  export class Sidebar {
945
- constructor(sovads, containerId) {
1345
+ constructor(sovads, containerId, slotConfig = {}) {
946
1346
  this.currentAd = null;
947
1347
  this.renderStartTime = 0;
948
1348
  this.hasTrackedImpression = false;
949
1349
  this.isRendering = false;
1350
+ this.refreshTimer = null;
1351
+ this.lastAdId = null;
1352
+ this.retryCount = 0;
1353
+ this.maxRetries = 3;
950
1354
  this.sovads = sovads;
951
1355
  this.containerId = containerId;
1356
+ this.slotConfig = slotConfig;
952
1357
  }
953
- async render(consumerId) {
1358
+ async render(consumerId, forceRefresh = false) {
954
1359
  // Prevent concurrent renders
955
- if (this.isRendering) {
1360
+ if (this.isRendering && !forceRefresh) {
956
1361
  if (this.sovads.getConfig().debug) {
957
1362
  console.warn(`Sidebar render already in progress for ${this.containerId}`);
958
1363
  }
@@ -966,8 +1371,32 @@ export class Sidebar {
966
1371
  this.isRendering = false;
967
1372
  return;
968
1373
  }
1374
+ // Lazy loading: wait for container to be in viewport
1375
+ if (this.sovads.getConfig().lazyLoad && !forceRefresh) {
1376
+ const isInViewport = await this.checkViewport(container);
1377
+ if (!isInViewport) {
1378
+ this.setupLazyLoadObserver(container, consumerId);
1379
+ this.isRendering = false;
1380
+ return;
1381
+ }
1382
+ }
969
1383
  this.renderStartTime = Date.now();
970
- this.currentAd = await this.sovads.loadAd(consumerId);
1384
+ this.currentAd = await this.sovads.loadAd({
1385
+ consumerId,
1386
+ placement: this.slotConfig.placementId || 'sidebar',
1387
+ size: this.slotConfig.size,
1388
+ });
1389
+ this.hasTrackedImpression = false;
1390
+ // Skip if same ad (rotation disabled or same ad returned)
1391
+ if (!forceRefresh && this.lastAdId === this.currentAd?.id && this.sovads.getConfig().rotationEnabled) {
1392
+ if (this.sovads.getConfig().debug) {
1393
+ console.log('Same ad returned, skipping render');
1394
+ }
1395
+ this.isRendering = false;
1396
+ return;
1397
+ }
1398
+ this.lastAdId = this.currentAd?.id || null;
1399
+ this.retryCount = 0;
971
1400
  if (!this.currentAd) {
972
1401
  container.innerHTML = '<div class="sovads-no-ad">No ads available</div>';
973
1402
  this.isRendering = false;
@@ -975,6 +1404,7 @@ export class Sidebar {
975
1404
  }
976
1405
  // Handle dummy ads for unregistered sites
977
1406
  if (this.currentAd.isDummy) {
1407
+ container.innerHTML = '';
978
1408
  const dummyElement = document.createElement('div');
979
1409
  dummyElement.className = 'sovads-sidebar-dummy';
980
1410
  dummyElement.setAttribute('data-ad-id', this.currentAd.id);
@@ -1021,18 +1451,20 @@ export class Sidebar {
1021
1451
  return;
1022
1452
  }
1023
1453
  const adElement = document.createElement('div');
1454
+ container.innerHTML = '';
1024
1455
  adElement.className = 'sovads-sidebar';
1025
1456
  adElement.setAttribute('data-ad-id', this.currentAd.id);
1457
+ const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
1026
1458
  adElement.style.cssText = `
1027
1459
  background: #f8f9fa;
1028
1460
  border: 1px solid #e9ecef;
1029
1461
  border-radius: 8px;
1030
1462
  padding: 15px;
1031
1463
  margin-bottom: 15px;
1032
- cursor: pointer;
1464
+ cursor: ${mediaType === 'video' ? 'default' : 'pointer'};
1033
1465
  transition: all 0.2s ease;
1466
+ opacity: 0;
1034
1467
  `;
1035
- const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
1036
1468
  const handleVisibilityTracking = (renderInfo) => {
1037
1469
  this.sovads.setupRenderObserver(adElement, this.currentAd.id, (isVisible) => {
1038
1470
  renderInfo.viewportVisible = isVisible;
@@ -1043,6 +1475,7 @@ export class Sidebar {
1043
1475
  });
1044
1476
  };
1045
1477
  const handleRenderSuccess = () => {
1478
+ adElement.style.opacity = '1';
1046
1479
  const renderTime = Date.now() - this.renderStartTime;
1047
1480
  handleVisibilityTracking({
1048
1481
  rendered: true,
@@ -1051,6 +1484,7 @@ export class Sidebar {
1051
1484
  });
1052
1485
  };
1053
1486
  const handleRenderError = () => {
1487
+ adElement.style.opacity = '1';
1054
1488
  if (this.sovads.getConfig().debug) {
1055
1489
  console.warn(`Failed to load sidebar ad media: ${this.currentAd.bannerUrl}`);
1056
1490
  }
@@ -1083,14 +1517,12 @@ export class Sidebar {
1083
1517
  img.addEventListener('error', handleRenderError, { once: true });
1084
1518
  mediaElement = img;
1085
1519
  }
1086
- // Add click handler
1087
- adElement.addEventListener('click', () => {
1520
+ const handleClickThrough = () => {
1088
1521
  this.sovads._trackEvent('CLICK', this.currentAd.id, this.currentAd.campaignId, {
1089
1522
  rendered: true,
1090
1523
  viewportVisible: true,
1091
1524
  renderTime: Date.now() - this.renderStartTime
1092
1525
  });
1093
- // Log interaction
1094
1526
  this.sovads.logInteraction('CLICK', {
1095
1527
  adId: this.currentAd.id,
1096
1528
  campaignId: this.currentAd.campaignId,
@@ -1098,7 +1530,7 @@ export class Sidebar {
1098
1530
  metadata: { renderTime: Date.now() - this.renderStartTime },
1099
1531
  });
1100
1532
  window.open(this.sovads.normalizeUrl(this.currentAd.targetUrl), '_blank', 'noopener,noreferrer');
1101
- });
1533
+ };
1102
1534
  // Add hover effect
1103
1535
  adElement.addEventListener('mouseenter', () => {
1104
1536
  adElement.style.background = '#e9ecef';
@@ -1108,14 +1540,115 @@ export class Sidebar {
1108
1540
  adElement.style.background = '#f8f9fa';
1109
1541
  adElement.style.transform = 'translateY(0)';
1110
1542
  });
1111
- mediaElement.style.cursor = 'pointer';
1112
- adElement.appendChild(mediaElement);
1543
+ mediaElement.style.cursor = mediaType === 'video' ? 'default' : 'pointer';
1544
+ if (mediaType === 'video') {
1545
+ const ctaButton = document.createElement('button');
1546
+ ctaButton.type = 'button';
1547
+ ctaButton.textContent = 'Learn more';
1548
+ ctaButton.style.cssText = `
1549
+ width: 100%;
1550
+ border: none;
1551
+ margin-top: 8px;
1552
+ background: #111;
1553
+ color: #fff;
1554
+ font-size: 12px;
1555
+ font-weight: 600;
1556
+ padding: 8px 12px;
1557
+ border-radius: 6px;
1558
+ cursor: pointer;
1559
+ `;
1560
+ ctaButton.addEventListener('click', handleClickThrough);
1561
+ adElement.appendChild(mediaElement);
1562
+ adElement.appendChild(ctaButton);
1563
+ }
1564
+ else {
1565
+ adElement.addEventListener('click', handleClickThrough);
1566
+ adElement.appendChild(mediaElement);
1567
+ }
1113
1568
  container.appendChild(adElement);
1569
+ // Set up auto-refresh if enabled
1570
+ this.setupAutoRefresh(consumerId);
1571
+ }
1572
+ catch (error) {
1573
+ // Retry logic on error
1574
+ if (this.retryCount < this.maxRetries) {
1575
+ this.retryCount++;
1576
+ if (this.sovads.getConfig().debug) {
1577
+ console.warn(`Sidebar render failed, retrying (${this.retryCount}/${this.maxRetries})...`);
1578
+ }
1579
+ await new Promise(resolve => setTimeout(resolve, 1000 * this.retryCount));
1580
+ this.isRendering = false;
1581
+ return this.render(consumerId, true);
1582
+ }
1583
+ else {
1584
+ const container = document.getElementById(this.containerId);
1585
+ if (container) {
1586
+ container.innerHTML = '<div class="sovads-error" style="padding: 10px; text-align: center; color: #666; font-size: 12px;">Ad temporarily unavailable</div>';
1587
+ }
1588
+ if (this.sovads.getConfig().debug) {
1589
+ console.error('Sidebar render failed after retries:', error);
1590
+ }
1591
+ }
1114
1592
  }
1115
1593
  finally {
1116
1594
  this.isRendering = false;
1117
1595
  }
1118
1596
  }
1597
+ async checkViewport(element) {
1598
+ return new Promise((resolve) => {
1599
+ if (typeof IntersectionObserver === 'undefined') {
1600
+ resolve(true);
1601
+ return;
1602
+ }
1603
+ const observer = new IntersectionObserver((entries) => {
1604
+ entries.forEach((entry) => {
1605
+ if (entry.isIntersecting) {
1606
+ observer.disconnect();
1607
+ resolve(true);
1608
+ }
1609
+ });
1610
+ }, { rootMargin: '50px' });
1611
+ observer.observe(element);
1612
+ setTimeout(() => {
1613
+ observer.disconnect();
1614
+ resolve(true);
1615
+ }, 5000);
1616
+ });
1617
+ }
1618
+ setupLazyLoadObserver(container, consumerId) {
1619
+ if (typeof IntersectionObserver === 'undefined') {
1620
+ this.render(consumerId);
1621
+ return;
1622
+ }
1623
+ const observer = new IntersectionObserver((entries) => {
1624
+ entries.forEach((entry) => {
1625
+ if (entry.isIntersecting && !this.isRendering) {
1626
+ observer.disconnect();
1627
+ this.render(consumerId);
1628
+ }
1629
+ });
1630
+ }, { rootMargin: '50px' });
1631
+ observer.observe(container);
1632
+ }
1633
+ setupAutoRefresh(consumerId) {
1634
+ if (this.refreshTimer) {
1635
+ clearInterval(this.refreshTimer);
1636
+ }
1637
+ const refreshInterval = this.sovads.getConfig().refreshInterval || 0;
1638
+ if (refreshInterval > 0) {
1639
+ this.refreshTimer = window.setInterval(() => {
1640
+ if (!this.isRendering) {
1641
+ this.render(consumerId, true);
1642
+ }
1643
+ }, refreshInterval * 1000);
1644
+ }
1645
+ }
1646
+ destroy() {
1647
+ if (this.refreshTimer) {
1648
+ clearInterval(this.refreshTimer);
1649
+ this.refreshTimer = null;
1650
+ }
1651
+ }
1119
1652
  }
1120
1653
  // Export main SovAds class
1121
1654
  export { SovAds };