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.d.ts +53 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +665 -132
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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 =
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
247
|
+
const trimmed = url.trim();
|
|
248
|
+
if (!trimmed.includes('://')) {
|
|
220
249
|
// Allow localhost URLs without protocol for debugging
|
|
221
|
-
if (
|
|
222
|
-
return `http://${
|
|
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
|
|
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(
|
|
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,
|
|
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'
|
|
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
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
409
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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.
|
|
589
|
+
console.log(`SovAds: Tracked ${type} event via signed beacon`, {
|
|
590
|
+
payload: { ...payload, fingerprint: payload.fingerprint.substring(0, 8) + '...' }
|
|
591
|
+
});
|
|
464
592
|
}
|
|
465
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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(
|
|
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
|
-
|
|
698
|
-
|
|
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.
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
|
1093
|
+
rendered,
|
|
767
1094
|
viewportVisible: true,
|
|
768
|
-
renderTime
|
|
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
|
|
1101
|
+
metadata: { renderTime, rendered },
|
|
776
1102
|
});
|
|
777
|
-
}
|
|
778
|
-
// Create
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
width:
|
|
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:
|
|
799
|
-
max-width:
|
|
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
|
-
|
|
850
|
-
document.body.appendChild(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
922
|
-
|
|
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
|
|
1323
|
+
if (overlay) {
|
|
931
1324
|
try {
|
|
932
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|