sovads-sdk 1.0.3 → 1.0.5
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/README.md +15 -0
- package/dist/index.d.ts +74 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +882 -138
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -5,31 +5,97 @@ 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();
|
|
10
|
+
this.walletAddress = null;
|
|
9
11
|
this.config = {
|
|
10
12
|
apiUrl: typeof window !== 'undefined' && window.location.hostname === 'localhost'
|
|
11
13
|
? 'http://localhost:3000'
|
|
12
14
|
: 'https://ads.sovseas.xyz',
|
|
13
15
|
debug: false,
|
|
16
|
+
refreshInterval: 0, // No auto-refresh by default
|
|
17
|
+
lazyLoad: true,
|
|
18
|
+
rotationEnabled: true,
|
|
19
|
+
popupMinIntervalMinutes: 30,
|
|
20
|
+
popupSessionMax: 1,
|
|
14
21
|
...config
|
|
15
22
|
};
|
|
23
|
+
this.debugLoggingEnabled = Boolean(this.config.debug);
|
|
16
24
|
this.fingerprint = this.generateFingerprint();
|
|
25
|
+
// Load persisted wallet address if available
|
|
26
|
+
this.loadPersistedIdentity();
|
|
27
|
+
if (this.config.walletAddress) {
|
|
28
|
+
this.identify(this.config.walletAddress);
|
|
29
|
+
}
|
|
17
30
|
if (this.config.debug) {
|
|
18
31
|
console.log('SovAds SDK initialized:', this.config);
|
|
19
32
|
}
|
|
20
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Identifies the current viewer with a wallet address.
|
|
36
|
+
* This links the device fingerprint to the wallet on the backend.
|
|
37
|
+
*/
|
|
38
|
+
identify(walletAddress) {
|
|
39
|
+
if (!walletAddress || typeof walletAddress !== 'string')
|
|
40
|
+
return;
|
|
41
|
+
this.walletAddress = walletAddress.toLowerCase();
|
|
42
|
+
try {
|
|
43
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
44
|
+
localStorage.setItem('sovads_wallet_address', this.walletAddress);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
// Ignore storage errors
|
|
49
|
+
}
|
|
50
|
+
if (this.config.debug) {
|
|
51
|
+
console.log('SovAds Identity set:', this.walletAddress);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
loadPersistedIdentity() {
|
|
55
|
+
try {
|
|
56
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
57
|
+
const saved = localStorage.getItem('sovads_wallet_address');
|
|
58
|
+
if (saved) {
|
|
59
|
+
this.walletAddress = saved.toLowerCase();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
// Ignore storage errors
|
|
65
|
+
}
|
|
66
|
+
}
|
|
21
67
|
generateFingerprint() {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
68
|
+
const storageKey = 'sovads_fingerprint_v1';
|
|
69
|
+
try {
|
|
70
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
71
|
+
const existing = window.localStorage.getItem(storageKey);
|
|
72
|
+
if (existing) {
|
|
73
|
+
return existing;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Ignore storage access errors and fall back to generated value.
|
|
79
|
+
}
|
|
80
|
+
const browserParts = [
|
|
81
|
+
typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown-ua',
|
|
82
|
+
typeof navigator !== 'undefined' ? navigator.language : 'unknown-lang',
|
|
83
|
+
typeof screen !== 'undefined' ? `${screen.width}x${screen.height}` : 'unknown-screen',
|
|
84
|
+
String(new Date().getTimezoneOffset()),
|
|
85
|
+
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
86
|
+
? crypto.randomUUID()
|
|
87
|
+
: `${Date.now()}-${Math.random()}`,
|
|
88
|
+
];
|
|
89
|
+
const value = btoa(browserParts.join('|')).replace(/=+$/g, '');
|
|
90
|
+
try {
|
|
91
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
92
|
+
window.localStorage.setItem(storageKey, value);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Ignore storage write failures.
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
33
99
|
}
|
|
34
100
|
async detectSiteId() {
|
|
35
101
|
if (this.siteId) {
|
|
@@ -47,6 +113,7 @@ class SovAds {
|
|
|
47
113
|
const domain = window.location.hostname;
|
|
48
114
|
const payload = {
|
|
49
115
|
domain,
|
|
116
|
+
pathname: window.location.pathname,
|
|
50
117
|
fingerprint: this.fingerprint,
|
|
51
118
|
userAgent: navigator.userAgent,
|
|
52
119
|
pageUrl: window.location.href,
|
|
@@ -216,13 +283,16 @@ class SovAds {
|
|
|
216
283
|
* Normalize URL - add protocol if missing for localhost
|
|
217
284
|
*/
|
|
218
285
|
normalizeUrl(url) {
|
|
219
|
-
|
|
286
|
+
const trimmed = url.trim();
|
|
287
|
+
if (!trimmed.includes('://')) {
|
|
220
288
|
// Allow localhost URLs without protocol for debugging
|
|
221
|
-
if (
|
|
222
|
-
return `http://${
|
|
289
|
+
if (trimmed.startsWith('localhost') || trimmed.startsWith('127.0.0.1')) {
|
|
290
|
+
return `http://${trimmed}`;
|
|
223
291
|
}
|
|
292
|
+
// Treat bare domains as https by default.
|
|
293
|
+
return `https://${trimmed}`;
|
|
224
294
|
}
|
|
225
|
-
return
|
|
295
|
+
return trimmed;
|
|
226
296
|
}
|
|
227
297
|
/**
|
|
228
298
|
* Validate URL format
|
|
@@ -237,6 +307,11 @@ class SovAds {
|
|
|
237
307
|
return false;
|
|
238
308
|
}
|
|
239
309
|
}
|
|
310
|
+
inferMediaTypeFromUrl(url) {
|
|
311
|
+
const value = (url || '').toLowerCase();
|
|
312
|
+
const videoExts = ['.mp4', '.webm', '.ogg', '.ogv', '.mov', '.m3u8'];
|
|
313
|
+
return videoExts.some((ext) => value.includes(ext)) ? 'video' : 'image';
|
|
314
|
+
}
|
|
240
315
|
/**
|
|
241
316
|
* Fetch with retry logic
|
|
242
317
|
*/
|
|
@@ -263,15 +338,27 @@ class SovAds {
|
|
|
263
338
|
}
|
|
264
339
|
throw lastError || new Error('Fetch failed after retries');
|
|
265
340
|
}
|
|
266
|
-
async loadAd(
|
|
341
|
+
async loadAd(options = {}) {
|
|
267
342
|
const startTime = Date.now();
|
|
268
343
|
try {
|
|
269
344
|
const siteId = await this.detectSiteId();
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
345
|
+
const url = new URL(`${this.config.apiUrl}/api/ads`);
|
|
346
|
+
url.searchParams.append('siteId', siteId);
|
|
347
|
+
if (options.consumerId || this.config.consumerId) {
|
|
348
|
+
url.searchParams.append('consumerId', (options.consumerId || this.config.consumerId));
|
|
349
|
+
}
|
|
350
|
+
if (options.placement) {
|
|
351
|
+
url.searchParams.append('placement', options.placement);
|
|
352
|
+
}
|
|
353
|
+
if (options.size) {
|
|
354
|
+
url.searchParams.append('size', options.size);
|
|
355
|
+
}
|
|
356
|
+
// Add wallet address for targeting and attribution
|
|
357
|
+
const wallet = options.walletAddress || this.walletAddress;
|
|
358
|
+
if (wallet) {
|
|
359
|
+
url.searchParams.append('wallet', wallet);
|
|
360
|
+
}
|
|
361
|
+
const endpoint = url.toString();
|
|
275
362
|
const response = await this.fetchWithRetry(endpoint);
|
|
276
363
|
const duration = Date.now() - startTime;
|
|
277
364
|
// Log SDK request
|
|
@@ -284,7 +371,7 @@ class SovAds {
|
|
|
284
371
|
pageUrl: window.location.href,
|
|
285
372
|
userAgent: navigator.userAgent,
|
|
286
373
|
fingerprint: this.fingerprint,
|
|
287
|
-
requestBody: { siteId,
|
|
374
|
+
requestBody: { siteId, ...options },
|
|
288
375
|
responseStatus: response.status,
|
|
289
376
|
duration,
|
|
290
377
|
});
|
|
@@ -335,8 +422,13 @@ class SovAds {
|
|
|
335
422
|
...rawAd,
|
|
336
423
|
bannerUrl: this.normalizeUrl(rawAd.bannerUrl),
|
|
337
424
|
targetUrl: this.normalizeUrl(rawAd.targetUrl),
|
|
338
|
-
mediaType: rawAd.mediaType === 'video'
|
|
425
|
+
mediaType: rawAd.mediaType === 'video'
|
|
426
|
+
? 'video'
|
|
427
|
+
: this.inferMediaTypeFromUrl(this.normalizeUrl(rawAd.bannerUrl)),
|
|
339
428
|
};
|
|
429
|
+
if (normalizedAd.trackingToken) {
|
|
430
|
+
this.adTrackingTokens.set(normalizedAd.id, normalizedAd.trackingToken);
|
|
431
|
+
}
|
|
340
432
|
if (this.config.debug) {
|
|
341
433
|
console.log('Ad loaded:', normalizedAd);
|
|
342
434
|
}
|
|
@@ -370,6 +462,105 @@ class SovAds {
|
|
|
370
462
|
return null;
|
|
371
463
|
}
|
|
372
464
|
}
|
|
465
|
+
toBase64(bytes) {
|
|
466
|
+
let binary = '';
|
|
467
|
+
for (const b of bytes) {
|
|
468
|
+
binary += String.fromCharCode(b);
|
|
469
|
+
}
|
|
470
|
+
return btoa(binary);
|
|
471
|
+
}
|
|
472
|
+
async signTrackingPayload(payload, timestamp) {
|
|
473
|
+
if (!this.config.apiSecret || typeof crypto === 'undefined' || !crypto.subtle) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
const encoder = new TextEncoder();
|
|
478
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(this.config.apiSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
479
|
+
const message = `${timestamp}:${payload}`;
|
|
480
|
+
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(message));
|
|
481
|
+
return this.toBase64(new Uint8Array(signature));
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
if (this.config.debug) {
|
|
485
|
+
console.error('Failed to sign tracking payload:', error);
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async sendTrackingEnvelope(eventPayload, useBeacon) {
|
|
491
|
+
if (eventPayload.trackingToken) {
|
|
492
|
+
const tokenBody = JSON.stringify({
|
|
493
|
+
trackingToken: eventPayload.trackingToken,
|
|
494
|
+
payload: eventPayload,
|
|
495
|
+
});
|
|
496
|
+
const tokenWebhookUrl = `${this.config.apiUrl}/api/webhook/track`;
|
|
497
|
+
try {
|
|
498
|
+
if (useBeacon && navigator.sendBeacon) {
|
|
499
|
+
return navigator.sendBeacon(tokenWebhookUrl, new Blob([tokenBody], { type: 'application/json' }));
|
|
500
|
+
}
|
|
501
|
+
const response = await fetch(tokenWebhookUrl, {
|
|
502
|
+
method: 'POST',
|
|
503
|
+
headers: { 'Content-Type': 'application/json' },
|
|
504
|
+
body: tokenBody,
|
|
505
|
+
keepalive: true,
|
|
506
|
+
});
|
|
507
|
+
return response.ok;
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (!this.config.apiKey || !this.config.apiSecret) {
|
|
514
|
+
if (this.config.debug) {
|
|
515
|
+
const devWebhookUrl = `${this.config.apiUrl}/api/webhook/beacon`;
|
|
516
|
+
const body = JSON.stringify(eventPayload);
|
|
517
|
+
try {
|
|
518
|
+
if (useBeacon && navigator.sendBeacon) {
|
|
519
|
+
return navigator.sendBeacon(devWebhookUrl, new Blob([body], { type: 'application/json' }));
|
|
520
|
+
}
|
|
521
|
+
const response = await fetch(devWebhookUrl, {
|
|
522
|
+
method: 'POST',
|
|
523
|
+
headers: { 'Content-Type': 'application/json' },
|
|
524
|
+
body,
|
|
525
|
+
keepalive: true,
|
|
526
|
+
});
|
|
527
|
+
return response.ok;
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
console.warn('SovAds: Missing apiKey/apiSecret, skipping signed tracking event');
|
|
535
|
+
}
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
const timestamp = Date.now();
|
|
539
|
+
const payload = JSON.stringify(eventPayload);
|
|
540
|
+
const signature = await this.signTrackingPayload(payload, timestamp);
|
|
541
|
+
if (!signature) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
const envelope = JSON.stringify({
|
|
545
|
+
apiKey: this.config.apiKey,
|
|
546
|
+
siteId: eventPayload.siteId,
|
|
547
|
+
payload,
|
|
548
|
+
signature,
|
|
549
|
+
timestamp,
|
|
550
|
+
});
|
|
551
|
+
const webhookUrl = `${this.config.apiUrl}/api/webhook/track`;
|
|
552
|
+
if (useBeacon && navigator.sendBeacon) {
|
|
553
|
+
const blob = new Blob([envelope], { type: 'application/json' });
|
|
554
|
+
return navigator.sendBeacon(webhookUrl, blob);
|
|
555
|
+
}
|
|
556
|
+
const response = await fetch(webhookUrl, {
|
|
557
|
+
method: 'POST',
|
|
558
|
+
headers: { 'Content-Type': 'application/json' },
|
|
559
|
+
body: envelope,
|
|
560
|
+
keepalive: true,
|
|
561
|
+
});
|
|
562
|
+
return response.ok;
|
|
563
|
+
}
|
|
373
564
|
/**
|
|
374
565
|
* Track event with retry logic (internal helper)
|
|
375
566
|
*/
|
|
@@ -389,24 +580,15 @@ class SovAds {
|
|
|
389
580
|
renderTime: renderInfo?.renderTime ?? Date.now(),
|
|
390
581
|
timestamp: metadata.timestamp,
|
|
391
582
|
pageUrl: metadata.pageUrl,
|
|
392
|
-
userAgent: metadata.userAgent
|
|
583
|
+
userAgent: metadata.userAgent,
|
|
584
|
+
trackingToken: this.adTrackingTokens.get(adId),
|
|
393
585
|
};
|
|
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
|
-
}
|
|
586
|
+
const ok = await this.sendTrackingEnvelope(payload, false);
|
|
587
|
+
if (!ok) {
|
|
588
|
+
throw new Error('Tracking endpoint rejected event');
|
|
407
589
|
}
|
|
408
|
-
|
|
409
|
-
|
|
590
|
+
if (this.config.debug) {
|
|
591
|
+
console.log(`SovAds: Tracked ${type} event via signed fetch (attempt ${attempt})`, payload);
|
|
410
592
|
}
|
|
411
593
|
}
|
|
412
594
|
catch (error) {
|
|
@@ -446,35 +628,26 @@ class SovAds {
|
|
|
446
628
|
renderTime: renderInfo?.renderTime ?? Date.now(),
|
|
447
629
|
timestamp: metadata.timestamp,
|
|
448
630
|
pageUrl: metadata.pageUrl,
|
|
449
|
-
userAgent: metadata.userAgent
|
|
631
|
+
userAgent: metadata.userAgent,
|
|
632
|
+
trackingToken: this.adTrackingTokens.get(adId),
|
|
633
|
+
walletAddress: this.walletAddress || undefined
|
|
450
634
|
};
|
|
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
|
|
635
|
+
if (typeof navigator.sendBeacon === 'function') {
|
|
636
|
+
const sent = await this.sendTrackingEnvelope(payload, true);
|
|
637
|
+
if (sent) {
|
|
462
638
|
if (this.config.debug) {
|
|
463
|
-
console.
|
|
639
|
+
console.log(`SovAds: Tracked ${type} event via signed beacon`, {
|
|
640
|
+
payload: { ...payload, fingerprint: payload.fingerprint.substring(0, 8) + '...' }
|
|
641
|
+
});
|
|
464
642
|
}
|
|
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
|
-
});
|
|
643
|
+
return;
|
|
472
644
|
}
|
|
473
645
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
646
|
+
// Fallback to signed fetch for older browsers and beacon failures
|
|
647
|
+
if (this.config.debug) {
|
|
648
|
+
console.warn(`SovAds: Beacon unavailable/failed for ${type}, falling back to signed fetch`);
|
|
477
649
|
}
|
|
650
|
+
await this.trackEventWithRetry(type, adId, campaignId, renderInfo, 1);
|
|
478
651
|
}
|
|
479
652
|
catch (error) {
|
|
480
653
|
if (this.config.debug) {
|
|
@@ -555,17 +728,22 @@ class SovAds {
|
|
|
555
728
|
}
|
|
556
729
|
// Banner Component
|
|
557
730
|
export class Banner {
|
|
558
|
-
constructor(sovads, containerId) {
|
|
731
|
+
constructor(sovads, containerId, slotConfig = {}) {
|
|
559
732
|
this.currentAd = null;
|
|
560
733
|
this.renderStartTime = 0;
|
|
561
734
|
this.hasTrackedImpression = false;
|
|
562
735
|
this.isRendering = false;
|
|
736
|
+
this.refreshTimer = null;
|
|
737
|
+
this.lastAdId = null;
|
|
738
|
+
this.retryCount = 0;
|
|
739
|
+
this.maxRetries = 3;
|
|
563
740
|
this.sovads = sovads;
|
|
564
741
|
this.containerId = containerId;
|
|
742
|
+
this.slotConfig = slotConfig;
|
|
565
743
|
}
|
|
566
|
-
async render(consumerId) {
|
|
744
|
+
async render(consumerId, forceRefresh = false) {
|
|
567
745
|
// Prevent concurrent renders
|
|
568
|
-
if (this.isRendering) {
|
|
746
|
+
if (this.isRendering && !forceRefresh) {
|
|
569
747
|
if (this.sovads.getConfig().debug) {
|
|
570
748
|
console.warn(`Banner render already in progress for ${this.containerId}`);
|
|
571
749
|
}
|
|
@@ -579,8 +757,33 @@ export class Banner {
|
|
|
579
757
|
this.isRendering = false;
|
|
580
758
|
return;
|
|
581
759
|
}
|
|
760
|
+
// Lazy loading: wait for container to be in viewport
|
|
761
|
+
if (this.sovads.getConfig().lazyLoad && !forceRefresh) {
|
|
762
|
+
const isInViewport = await this.checkViewport(container);
|
|
763
|
+
if (!isInViewport) {
|
|
764
|
+
// Set up intersection observer for lazy loading
|
|
765
|
+
this.setupLazyLoadObserver(container, consumerId);
|
|
766
|
+
this.isRendering = false;
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
582
770
|
this.renderStartTime = Date.now();
|
|
583
|
-
this.currentAd = await this.sovads.loadAd(
|
|
771
|
+
this.currentAd = await this.sovads.loadAd({
|
|
772
|
+
consumerId,
|
|
773
|
+
placement: this.slotConfig.placementId || 'banner',
|
|
774
|
+
size: this.slotConfig.size,
|
|
775
|
+
});
|
|
776
|
+
this.hasTrackedImpression = false;
|
|
777
|
+
// Skip if same ad (rotation disabled or same ad returned)
|
|
778
|
+
if (!forceRefresh && this.lastAdId === this.currentAd?.id && this.sovads.getConfig().rotationEnabled) {
|
|
779
|
+
if (this.sovads.getConfig().debug) {
|
|
780
|
+
console.log('Same ad returned, skipping render');
|
|
781
|
+
}
|
|
782
|
+
this.isRendering = false;
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
this.lastAdId = this.currentAd?.id || null;
|
|
786
|
+
this.retryCount = 0; // Reset retry count on success
|
|
584
787
|
if (!this.currentAd) {
|
|
585
788
|
container.innerHTML = '<div class="sovads-no-ad">No ads available</div>';
|
|
586
789
|
this.isRendering = false;
|
|
@@ -588,6 +791,7 @@ export class Banner {
|
|
|
588
791
|
}
|
|
589
792
|
// Handle dummy ads for unregistered sites
|
|
590
793
|
if (this.currentAd.isDummy) {
|
|
794
|
+
container.innerHTML = '';
|
|
591
795
|
const dummyElement = document.createElement('div');
|
|
592
796
|
dummyElement.className = 'sovads-banner-dummy';
|
|
593
797
|
dummyElement.setAttribute('data-ad-id', this.currentAd.id);
|
|
@@ -633,16 +837,21 @@ export class Banner {
|
|
|
633
837
|
return;
|
|
634
838
|
}
|
|
635
839
|
const adElement = document.createElement('div');
|
|
840
|
+
container.innerHTML = '';
|
|
636
841
|
adElement.className = 'sovads-banner';
|
|
637
842
|
adElement.setAttribute('data-ad-id', this.currentAd.id);
|
|
843
|
+
const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
|
|
638
844
|
adElement.style.cssText = `
|
|
639
845
|
border: 1px solid #333;
|
|
640
846
|
border-radius: 8px;
|
|
641
847
|
overflow: hidden;
|
|
642
|
-
cursor: pointer;
|
|
848
|
+
cursor: ${mediaType === 'video' ? 'default' : 'pointer'};
|
|
643
849
|
transition: transform 0.2s ease;
|
|
850
|
+
max-width: 100%;
|
|
851
|
+
width: 100%;
|
|
852
|
+
box-sizing: border-box;
|
|
853
|
+
opacity: 0;
|
|
644
854
|
`;
|
|
645
|
-
const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
|
|
646
855
|
const handleVisibilityTracking = (renderInfo) => {
|
|
647
856
|
this.sovads.setupRenderObserver(adElement, this.currentAd.id, (isVisible) => {
|
|
648
857
|
renderInfo.viewportVisible = isVisible;
|
|
@@ -653,6 +862,7 @@ export class Banner {
|
|
|
653
862
|
});
|
|
654
863
|
};
|
|
655
864
|
const handleRenderSuccess = () => {
|
|
865
|
+
adElement.style.opacity = '1';
|
|
656
866
|
const renderTime = Date.now() - this.renderStartTime;
|
|
657
867
|
handleVisibilityTracking({
|
|
658
868
|
rendered: true,
|
|
@@ -661,6 +871,7 @@ export class Banner {
|
|
|
661
871
|
});
|
|
662
872
|
};
|
|
663
873
|
const handleRenderError = () => {
|
|
874
|
+
adElement.style.opacity = '1';
|
|
664
875
|
if (this.sovads.getConfig().debug) {
|
|
665
876
|
console.warn(`Failed to load ad media: ${this.currentAd.bannerUrl}`);
|
|
666
877
|
}
|
|
@@ -688,20 +899,19 @@ export class Banner {
|
|
|
688
899
|
const img = document.createElement('img');
|
|
689
900
|
img.src = this.currentAd.bannerUrl;
|
|
690
901
|
img.alt = this.currentAd.description;
|
|
691
|
-
img.style.cssText = 'width: 100%; height: auto; display: block;';
|
|
902
|
+
img.style.cssText = 'width: 100%; height: auto; display: block; max-width: 100%; object-fit: contain;';
|
|
692
903
|
img.addEventListener('load', handleRenderSuccess, { once: true });
|
|
693
904
|
img.addEventListener('error', handleRenderError, { once: true });
|
|
694
905
|
mediaElement = img;
|
|
695
906
|
}
|
|
696
|
-
mediaElement.style.cursor = 'pointer';
|
|
697
|
-
|
|
698
|
-
|
|
907
|
+
mediaElement.style.cursor = mediaType === 'video' ? 'default' : 'pointer';
|
|
908
|
+
mediaElement.style.maxWidth = '100%';
|
|
909
|
+
const handleClickThrough = () => {
|
|
699
910
|
this.sovads._trackEvent('CLICK', this.currentAd.id, this.currentAd.campaignId, {
|
|
700
911
|
rendered: true,
|
|
701
912
|
viewportVisible: true,
|
|
702
913
|
renderTime: Date.now() - this.renderStartTime
|
|
703
914
|
});
|
|
704
|
-
// Log interaction
|
|
705
915
|
this.sovads.logInteraction('CLICK', {
|
|
706
916
|
adId: this.currentAd.id,
|
|
707
917
|
campaignId: this.currentAd.campaignId,
|
|
@@ -709,7 +919,30 @@ export class Banner {
|
|
|
709
919
|
metadata: { renderTime: Date.now() - this.renderStartTime },
|
|
710
920
|
});
|
|
711
921
|
window.open(this.sovads.normalizeUrl(this.currentAd.targetUrl), '_blank', 'noopener,noreferrer');
|
|
712
|
-
}
|
|
922
|
+
};
|
|
923
|
+
if (mediaType === 'video') {
|
|
924
|
+
const ctaButton = document.createElement('button');
|
|
925
|
+
ctaButton.type = 'button';
|
|
926
|
+
ctaButton.textContent = 'Learn more';
|
|
927
|
+
ctaButton.style.cssText = `
|
|
928
|
+
width: 100%;
|
|
929
|
+
border: none;
|
|
930
|
+
border-top: 1px solid #333;
|
|
931
|
+
background: #111;
|
|
932
|
+
color: #fff;
|
|
933
|
+
font-size: 12px;
|
|
934
|
+
font-weight: 600;
|
|
935
|
+
padding: 8px 12px;
|
|
936
|
+
cursor: pointer;
|
|
937
|
+
`;
|
|
938
|
+
ctaButton.addEventListener('click', handleClickThrough);
|
|
939
|
+
adElement.appendChild(mediaElement);
|
|
940
|
+
adElement.appendChild(ctaButton);
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
adElement.addEventListener('click', handleClickThrough);
|
|
944
|
+
adElement.appendChild(mediaElement);
|
|
945
|
+
}
|
|
713
946
|
// Add hover effect
|
|
714
947
|
adElement.addEventListener('mouseenter', () => {
|
|
715
948
|
adElement.style.transform = 'scale(1.02)';
|
|
@@ -717,13 +950,94 @@ export class Banner {
|
|
|
717
950
|
adElement.addEventListener('mouseleave', () => {
|
|
718
951
|
adElement.style.transform = 'scale(1)';
|
|
719
952
|
});
|
|
720
|
-
adElement.appendChild(mediaElement);
|
|
721
953
|
container.appendChild(adElement);
|
|
954
|
+
// Set up auto-refresh if enabled
|
|
955
|
+
this.setupAutoRefresh(consumerId);
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
// Retry logic on error
|
|
959
|
+
if (this.retryCount < this.maxRetries) {
|
|
960
|
+
this.retryCount++;
|
|
961
|
+
if (this.sovads.getConfig().debug) {
|
|
962
|
+
console.warn(`Banner render failed, retrying (${this.retryCount}/${this.maxRetries})...`);
|
|
963
|
+
}
|
|
964
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * this.retryCount)); // Exponential backoff
|
|
965
|
+
this.isRendering = false;
|
|
966
|
+
return this.render(consumerId, true);
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
const container = document.getElementById(this.containerId);
|
|
970
|
+
if (container) {
|
|
971
|
+
container.innerHTML = '<div class="sovads-error" style="padding: 10px; text-align: center; color: #666; font-size: 12px;">Ad temporarily unavailable</div>';
|
|
972
|
+
}
|
|
973
|
+
if (this.sovads.getConfig().debug) {
|
|
974
|
+
console.error('Banner render failed after retries:', error);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
722
977
|
}
|
|
723
978
|
finally {
|
|
724
979
|
this.isRendering = false;
|
|
725
980
|
}
|
|
726
981
|
}
|
|
982
|
+
async checkViewport(element) {
|
|
983
|
+
return new Promise((resolve) => {
|
|
984
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
985
|
+
resolve(true); // Fallback: load immediately if IntersectionObserver not supported
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
const observer = new IntersectionObserver((entries) => {
|
|
989
|
+
entries.forEach((entry) => {
|
|
990
|
+
if (entry.isIntersecting) {
|
|
991
|
+
observer.disconnect();
|
|
992
|
+
resolve(true);
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
}, { rootMargin: '50px' } // Start loading 50px before entering viewport
|
|
996
|
+
);
|
|
997
|
+
observer.observe(element);
|
|
998
|
+
// Timeout after 5 seconds - load anyway
|
|
999
|
+
setTimeout(() => {
|
|
1000
|
+
observer.disconnect();
|
|
1001
|
+
resolve(true);
|
|
1002
|
+
}, 5000);
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
setupLazyLoadObserver(container, consumerId) {
|
|
1006
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
1007
|
+
// Fallback: load immediately
|
|
1008
|
+
this.render(consumerId);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const observer = new IntersectionObserver((entries) => {
|
|
1012
|
+
entries.forEach((entry) => {
|
|
1013
|
+
if (entry.isIntersecting && !this.isRendering) {
|
|
1014
|
+
observer.disconnect();
|
|
1015
|
+
this.render(consumerId);
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
}, { rootMargin: '50px' });
|
|
1019
|
+
observer.observe(container);
|
|
1020
|
+
}
|
|
1021
|
+
setupAutoRefresh(consumerId) {
|
|
1022
|
+
// Clear existing timer
|
|
1023
|
+
if (this.refreshTimer) {
|
|
1024
|
+
clearInterval(this.refreshTimer);
|
|
1025
|
+
}
|
|
1026
|
+
const refreshInterval = this.sovads.getConfig().refreshInterval || 0;
|
|
1027
|
+
if (refreshInterval > 0) {
|
|
1028
|
+
this.refreshTimer = window.setInterval(() => {
|
|
1029
|
+
if (!this.isRendering) {
|
|
1030
|
+
this.render(consumerId, true);
|
|
1031
|
+
}
|
|
1032
|
+
}, refreshInterval * 1000);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
destroy() {
|
|
1036
|
+
if (this.refreshTimer) {
|
|
1037
|
+
clearInterval(this.refreshTimer);
|
|
1038
|
+
this.refreshTimer = null;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
727
1041
|
}
|
|
728
1042
|
// Popup Component
|
|
729
1043
|
export class Popup {
|
|
@@ -731,8 +1045,40 @@ export class Popup {
|
|
|
731
1045
|
this.currentAd = null;
|
|
732
1046
|
this.popupElement = null;
|
|
733
1047
|
this.isShowing = false;
|
|
1048
|
+
this.retryCount = 0;
|
|
1049
|
+
this.maxRetries = 3;
|
|
1050
|
+
this.storageKeyLastShown = 'sovads_popup_last_shown';
|
|
1051
|
+
this.storageKeySessionCount = 'sovads_popup_session_count';
|
|
734
1052
|
this.sovads = sovads;
|
|
735
1053
|
}
|
|
1054
|
+
canShowByFrequencyCap() {
|
|
1055
|
+
try {
|
|
1056
|
+
const minIntervalMs = (this.sovads.getConfig().popupMinIntervalMinutes || 30) * 60 * 1000;
|
|
1057
|
+
const sessionMax = this.sovads.getConfig().popupSessionMax || 1;
|
|
1058
|
+
const now = Date.now();
|
|
1059
|
+
const lastShown = Number(localStorage.getItem(this.storageKeyLastShown) || 0);
|
|
1060
|
+
const sessionCount = Number(sessionStorage.getItem(this.storageKeySessionCount) || 0);
|
|
1061
|
+
if (sessionCount >= sessionMax)
|
|
1062
|
+
return false;
|
|
1063
|
+
if (lastShown > 0 && now - lastShown < minIntervalMs)
|
|
1064
|
+
return false;
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
catch {
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
markShown() {
|
|
1072
|
+
try {
|
|
1073
|
+
const now = Date.now();
|
|
1074
|
+
const currentSessionCount = Number(sessionStorage.getItem(this.storageKeySessionCount) || 0);
|
|
1075
|
+
localStorage.setItem(this.storageKeyLastShown, String(now));
|
|
1076
|
+
sessionStorage.setItem(this.storageKeySessionCount, String(currentSessionCount + 1));
|
|
1077
|
+
}
|
|
1078
|
+
catch {
|
|
1079
|
+
// Ignore storage access issues.
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
736
1082
|
async show(consumerId, delay = 3000) {
|
|
737
1083
|
// Prevent concurrent shows
|
|
738
1084
|
if (this.isShowing) {
|
|
@@ -741,65 +1087,113 @@ export class Popup {
|
|
|
741
1087
|
}
|
|
742
1088
|
return;
|
|
743
1089
|
}
|
|
744
|
-
this.
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
this.isShowing = false;
|
|
1090
|
+
if (!this.canShowByFrequencyCap()) {
|
|
1091
|
+
if (this.sovads.getConfig().debug) {
|
|
1092
|
+
console.log('Popup skipped due to frequency cap');
|
|
1093
|
+
}
|
|
749
1094
|
return;
|
|
750
1095
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
this.
|
|
1096
|
+
this.isShowing = true;
|
|
1097
|
+
try {
|
|
1098
|
+
this.currentAd = await this.sovads.loadAd({
|
|
1099
|
+
consumerId,
|
|
1100
|
+
placement: 'popup',
|
|
1101
|
+
size: window.innerWidth < 640 ? '320x100' : '360x120',
|
|
1102
|
+
});
|
|
1103
|
+
if (!this.currentAd) {
|
|
1104
|
+
if (this.retryCount < this.maxRetries) {
|
|
1105
|
+
this.retryCount++;
|
|
1106
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * this.retryCount));
|
|
1107
|
+
this.isShowing = false;
|
|
1108
|
+
return this.show(consumerId, delay);
|
|
1109
|
+
}
|
|
1110
|
+
if (this.sovads.getConfig().debug) {
|
|
1111
|
+
console.log('No popup ad available after retries');
|
|
1112
|
+
}
|
|
1113
|
+
this.isShowing = false;
|
|
1114
|
+
this.retryCount = 0;
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
this.retryCount = 0; // Reset on success
|
|
1118
|
+
// Show popup after delay
|
|
1119
|
+
setTimeout(() => {
|
|
1120
|
+
this.renderPopup();
|
|
1121
|
+
this.markShown();
|
|
1122
|
+
this.isShowing = false;
|
|
1123
|
+
}, delay);
|
|
1124
|
+
}
|
|
1125
|
+
catch (error) {
|
|
1126
|
+
if (this.sovads.getConfig().debug) {
|
|
1127
|
+
console.error('Error loading popup ad:', error);
|
|
1128
|
+
}
|
|
754
1129
|
this.isShowing = false;
|
|
755
|
-
|
|
1130
|
+
this.retryCount = 0;
|
|
1131
|
+
}
|
|
756
1132
|
}
|
|
757
1133
|
renderPopup() {
|
|
758
1134
|
if (!this.currentAd)
|
|
759
1135
|
return;
|
|
760
1136
|
const renderStartTime = Date.now();
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1137
|
+
let impressionTracked = false;
|
|
1138
|
+
const trackPopupImpression = (rendered, renderTime) => {
|
|
1139
|
+
if (impressionTracked || !this.currentAd || this.currentAd.isDummy)
|
|
1140
|
+
return;
|
|
1141
|
+
impressionTracked = true;
|
|
765
1142
|
this.sovads._trackEvent('IMPRESSION', this.currentAd.id, this.currentAd.campaignId, {
|
|
766
|
-
rendered
|
|
1143
|
+
rendered,
|
|
767
1144
|
viewportVisible: true,
|
|
768
|
-
renderTime
|
|
1145
|
+
renderTime,
|
|
769
1146
|
});
|
|
770
|
-
// Log interaction
|
|
771
1147
|
this.sovads.logInteraction('IMPRESSION', {
|
|
772
1148
|
adId: this.currentAd.id,
|
|
773
1149
|
campaignId: this.currentAd.campaignId,
|
|
774
1150
|
elementType: 'POPUP',
|
|
775
|
-
metadata: { renderTime
|
|
1151
|
+
metadata: { renderTime, rendered },
|
|
776
1152
|
});
|
|
777
|
-
}
|
|
778
|
-
// Create
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
1153
|
+
};
|
|
1154
|
+
// Create non-blocking sticky container
|
|
1155
|
+
const wrapper = document.createElement('div');
|
|
1156
|
+
wrapper.className = 'sovads-popup-overlay';
|
|
1157
|
+
wrapper.style.cssText = `
|
|
782
1158
|
position: fixed;
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
width:
|
|
786
|
-
height: 100%;
|
|
787
|
-
background: rgba(0, 0, 0, 0.5);
|
|
1159
|
+
right: 16px;
|
|
1160
|
+
bottom: 16px;
|
|
1161
|
+
width: min(360px, calc(100vw - 24px));
|
|
788
1162
|
z-index: 10000;
|
|
789
|
-
display: flex;
|
|
790
|
-
align-items: center;
|
|
791
|
-
justify-content: center;
|
|
792
1163
|
`;
|
|
793
1164
|
// Create popup
|
|
794
1165
|
this.popupElement = document.createElement('div');
|
|
795
1166
|
this.popupElement.style.cssText = `
|
|
796
1167
|
background: white;
|
|
797
1168
|
border-radius: 12px;
|
|
798
|
-
padding:
|
|
799
|
-
max-width:
|
|
1169
|
+
padding: 14px;
|
|
1170
|
+
max-width: 360px;
|
|
800
1171
|
position: relative;
|
|
801
1172
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
|
1173
|
+
opacity: 0;
|
|
1174
|
+
transition: opacity 0.2s ease;
|
|
802
1175
|
`;
|
|
1176
|
+
// SovAds logo badge in small left corner
|
|
1177
|
+
const logoBadge = document.createElement('div');
|
|
1178
|
+
logoBadge.style.cssText = `
|
|
1179
|
+
position: absolute;
|
|
1180
|
+
top: 8px;
|
|
1181
|
+
left: 12px;
|
|
1182
|
+
width: 24px;
|
|
1183
|
+
height: 24px;
|
|
1184
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
1185
|
+
border-radius: 4px;
|
|
1186
|
+
display: flex;
|
|
1187
|
+
align-items: center;
|
|
1188
|
+
justify-content: center;
|
|
1189
|
+
font-size: 10px;
|
|
1190
|
+
font-weight: bold;
|
|
1191
|
+
color: white;
|
|
1192
|
+
z-index: 1;
|
|
1193
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
1194
|
+
`;
|
|
1195
|
+
logoBadge.textContent = 'SA';
|
|
1196
|
+
logoBadge.title = 'SovAds';
|
|
803
1197
|
// Close button
|
|
804
1198
|
const closeBtn = document.createElement('button');
|
|
805
1199
|
closeBtn.innerHTML = '×';
|
|
@@ -812,10 +1206,24 @@ export class Popup {
|
|
|
812
1206
|
font-size: 24px;
|
|
813
1207
|
cursor: pointer;
|
|
814
1208
|
color: #666;
|
|
1209
|
+
z-index: 2;
|
|
815
1210
|
`;
|
|
816
1211
|
closeBtn.addEventListener('click', () => {
|
|
817
1212
|
this.hide();
|
|
818
1213
|
});
|
|
1214
|
+
// Add "Ad" message text below logo
|
|
1215
|
+
const adLabel = document.createElement('div');
|
|
1216
|
+
adLabel.style.cssText = `
|
|
1217
|
+
position: absolute;
|
|
1218
|
+
top: 36px;
|
|
1219
|
+
left: 12px;
|
|
1220
|
+
font-size: 9px;
|
|
1221
|
+
color: #999;
|
|
1222
|
+
font-weight: 500;
|
|
1223
|
+
text-transform: uppercase;
|
|
1224
|
+
letter-spacing: 0.5px;
|
|
1225
|
+
`;
|
|
1226
|
+
adLabel.textContent = 'Ad';
|
|
819
1227
|
// Handle dummy ads
|
|
820
1228
|
if (this.currentAd.isDummy) {
|
|
821
1229
|
const dummyContent = document.createElement('div');
|
|
@@ -844,10 +1252,12 @@ export class Popup {
|
|
|
844
1252
|
dummyContent.appendChild(img);
|
|
845
1253
|
dummyContent.appendChild(message);
|
|
846
1254
|
dummyContent.appendChild(link);
|
|
1255
|
+
this.popupElement.appendChild(logoBadge);
|
|
1256
|
+
this.popupElement.appendChild(adLabel);
|
|
847
1257
|
this.popupElement.appendChild(closeBtn);
|
|
848
1258
|
this.popupElement.appendChild(dummyContent);
|
|
849
|
-
|
|
850
|
-
document.body.appendChild(
|
|
1259
|
+
wrapper.appendChild(this.popupElement);
|
|
1260
|
+
document.body.appendChild(wrapper);
|
|
851
1261
|
// Auto close after 10 seconds
|
|
852
1262
|
setTimeout(() => {
|
|
853
1263
|
this.hide();
|
|
@@ -856,15 +1266,14 @@ export class Popup {
|
|
|
856
1266
|
}
|
|
857
1267
|
const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
|
|
858
1268
|
const handleMediaError = () => {
|
|
1269
|
+
if (this.popupElement) {
|
|
1270
|
+
this.popupElement.style.opacity = '1';
|
|
1271
|
+
}
|
|
859
1272
|
if (this.sovads.getConfig().debug) {
|
|
860
1273
|
console.warn(`Failed to load popup ad media: ${this.currentAd.bannerUrl}`);
|
|
861
1274
|
}
|
|
862
1275
|
const renderTime = Date.now() - renderStartTime;
|
|
863
|
-
|
|
864
|
-
rendered: false,
|
|
865
|
-
viewportVisible: true,
|
|
866
|
-
renderTime
|
|
867
|
-
});
|
|
1276
|
+
trackPopupImpression(false, renderTime);
|
|
868
1277
|
};
|
|
869
1278
|
let mediaElement;
|
|
870
1279
|
if (mediaType === 'video') {
|
|
@@ -877,7 +1286,11 @@ export class Popup {
|
|
|
877
1286
|
video.controls = true;
|
|
878
1287
|
video.style.cssText = 'width: 100%; height: auto; border-radius: 8px; cursor: pointer;';
|
|
879
1288
|
video.addEventListener('loadeddata', () => {
|
|
1289
|
+
if (this.popupElement) {
|
|
1290
|
+
this.popupElement.style.opacity = '1';
|
|
1291
|
+
}
|
|
880
1292
|
const renderTime = Date.now() - renderStartTime;
|
|
1293
|
+
trackPopupImpression(true, renderTime);
|
|
881
1294
|
if (this.sovads.getConfig().debug) {
|
|
882
1295
|
console.log(`Popup ad video loaded in ${renderTime}ms`);
|
|
883
1296
|
}
|
|
@@ -891,7 +1304,11 @@ export class Popup {
|
|
|
891
1304
|
img.alt = this.currentAd.description;
|
|
892
1305
|
img.style.cssText = 'width: 100%; height: auto; border-radius: 8px; cursor: pointer;';
|
|
893
1306
|
img.addEventListener('load', () => {
|
|
1307
|
+
if (this.popupElement) {
|
|
1308
|
+
this.popupElement.style.opacity = '1';
|
|
1309
|
+
}
|
|
894
1310
|
const renderTime = Date.now() - renderStartTime;
|
|
1311
|
+
trackPopupImpression(true, renderTime);
|
|
895
1312
|
if (this.sovads.getConfig().debug) {
|
|
896
1313
|
console.log(`Popup ad image loaded in ${renderTime}ms`);
|
|
897
1314
|
}
|
|
@@ -899,27 +1316,53 @@ export class Popup {
|
|
|
899
1316
|
img.addEventListener('error', handleMediaError);
|
|
900
1317
|
mediaElement = img;
|
|
901
1318
|
}
|
|
902
|
-
|
|
903
|
-
mediaElement.addEventListener('click', () => {
|
|
1319
|
+
const handleClickThrough = () => {
|
|
904
1320
|
this.sovads._trackEvent('CLICK', this.currentAd.id, this.currentAd.campaignId, {
|
|
905
1321
|
rendered: true,
|
|
906
1322
|
viewportVisible: true,
|
|
907
1323
|
renderTime: Date.now() - renderStartTime
|
|
908
1324
|
});
|
|
909
|
-
// Log interaction
|
|
910
1325
|
this.sovads.logInteraction('CLICK', {
|
|
911
1326
|
adId: this.currentAd.id,
|
|
912
1327
|
campaignId: this.currentAd.campaignId,
|
|
913
1328
|
elementType: 'POPUP',
|
|
914
1329
|
metadata: { renderTime: Date.now() - renderStartTime },
|
|
915
1330
|
});
|
|
916
|
-
window.open(this.currentAd.targetUrl, '_blank', 'noopener,noreferrer');
|
|
1331
|
+
window.open(this.sovads.normalizeUrl(this.currentAd.targetUrl), '_blank', 'noopener,noreferrer');
|
|
917
1332
|
this.hide();
|
|
918
|
-
}
|
|
1333
|
+
};
|
|
1334
|
+
if (mediaType === 'video') {
|
|
1335
|
+
mediaElement.style.cursor = 'default';
|
|
1336
|
+
}
|
|
1337
|
+
else {
|
|
1338
|
+
mediaElement.style.cursor = 'pointer';
|
|
1339
|
+
mediaElement.addEventListener('click', handleClickThrough);
|
|
1340
|
+
}
|
|
1341
|
+
this.popupElement.appendChild(logoBadge);
|
|
1342
|
+
this.popupElement.appendChild(adLabel);
|
|
919
1343
|
this.popupElement.appendChild(closeBtn);
|
|
920
1344
|
this.popupElement.appendChild(mediaElement);
|
|
921
|
-
|
|
922
|
-
|
|
1345
|
+
if (mediaType === 'video') {
|
|
1346
|
+
const ctaButton = document.createElement('button');
|
|
1347
|
+
ctaButton.type = 'button';
|
|
1348
|
+
ctaButton.textContent = 'Learn more';
|
|
1349
|
+
ctaButton.style.cssText = `
|
|
1350
|
+
width: 100%;
|
|
1351
|
+
margin-top: 10px;
|
|
1352
|
+
border: none;
|
|
1353
|
+
border-radius: 6px;
|
|
1354
|
+
background: #111;
|
|
1355
|
+
color: #fff;
|
|
1356
|
+
font-size: 12px;
|
|
1357
|
+
font-weight: 600;
|
|
1358
|
+
padding: 10px 12px;
|
|
1359
|
+
cursor: pointer;
|
|
1360
|
+
`;
|
|
1361
|
+
ctaButton.addEventListener('click', handleClickThrough);
|
|
1362
|
+
this.popupElement.appendChild(ctaButton);
|
|
1363
|
+
}
|
|
1364
|
+
wrapper.appendChild(this.popupElement);
|
|
1365
|
+
document.body.appendChild(wrapper);
|
|
923
1366
|
// Auto close after 10 seconds
|
|
924
1367
|
setTimeout(() => {
|
|
925
1368
|
this.hide();
|
|
@@ -927,32 +1370,207 @@ export class Popup {
|
|
|
927
1370
|
}
|
|
928
1371
|
hide() {
|
|
929
1372
|
const overlay = document.querySelector('.sovads-popup-overlay');
|
|
930
|
-
if (overlay
|
|
1373
|
+
if (overlay) {
|
|
931
1374
|
try {
|
|
932
|
-
|
|
1375
|
+
// Check if element is still connected to DOM before removing
|
|
1376
|
+
if (overlay.isConnected) {
|
|
1377
|
+
// Use remove() method which is safer and doesn't require parentNode
|
|
1378
|
+
overlay.remove();
|
|
1379
|
+
}
|
|
933
1380
|
}
|
|
934
1381
|
catch (error) {
|
|
935
1382
|
// Element may have already been removed by React or another process
|
|
1383
|
+
// Silently fail - this is expected in some cases
|
|
936
1384
|
if (this.sovads.getConfig().debug) {
|
|
937
1385
|
console.warn('Could not remove popup overlay:', error);
|
|
938
1386
|
}
|
|
939
1387
|
}
|
|
940
1388
|
}
|
|
1389
|
+
this.popupElement = null;
|
|
1390
|
+
this.currentAd = null;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
// BottomBar Component
|
|
1394
|
+
export class BottomBar {
|
|
1395
|
+
constructor(sovads) {
|
|
1396
|
+
this.barElement = null;
|
|
1397
|
+
this.currentAd = null;
|
|
1398
|
+
this.isVisible = false;
|
|
1399
|
+
this.retryCount = 0;
|
|
1400
|
+
this.maxRetries = 3;
|
|
1401
|
+
this.sovads = sovads;
|
|
1402
|
+
}
|
|
1403
|
+
async show(consumerId) {
|
|
1404
|
+
if (this.isVisible) {
|
|
1405
|
+
if (this.sovads.getConfig().debug) {
|
|
1406
|
+
console.warn('BottomBar already visible');
|
|
1407
|
+
}
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
try {
|
|
1411
|
+
this.currentAd = await this.sovads.loadAd({
|
|
1412
|
+
consumerId,
|
|
1413
|
+
placement: 'bottom-bar',
|
|
1414
|
+
size: 'full-width',
|
|
1415
|
+
});
|
|
1416
|
+
if (!this.currentAd) {
|
|
1417
|
+
if (this.retryCount < this.maxRetries) {
|
|
1418
|
+
this.retryCount++;
|
|
1419
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * this.retryCount));
|
|
1420
|
+
return this.show(consumerId);
|
|
1421
|
+
}
|
|
1422
|
+
if (this.sovads.getConfig().debug) {
|
|
1423
|
+
console.log('No bottom‑bar ad available after retries');
|
|
1424
|
+
}
|
|
1425
|
+
this.retryCount = 0;
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
this.retryCount = 0;
|
|
1429
|
+
this.renderBar();
|
|
1430
|
+
this.isVisible = true;
|
|
1431
|
+
}
|
|
1432
|
+
catch (error) {
|
|
1433
|
+
if (this.sovads.getConfig().debug) {
|
|
1434
|
+
console.error('Error loading bottom bar ad:', error);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
renderBar() {
|
|
1439
|
+
if (!this.currentAd)
|
|
1440
|
+
return;
|
|
1441
|
+
const renderStart = Date.now();
|
|
1442
|
+
let impressionTracked = false;
|
|
1443
|
+
const trackImp = (rendered, renderTime) => {
|
|
1444
|
+
if (impressionTracked || !this.currentAd || this.currentAd.isDummy)
|
|
1445
|
+
return;
|
|
1446
|
+
impressionTracked = true;
|
|
1447
|
+
this.sovads._trackEvent('IMPRESSION', this.currentAd.id, this.currentAd.campaignId, {
|
|
1448
|
+
rendered,
|
|
1449
|
+
viewportVisible: true,
|
|
1450
|
+
renderTime,
|
|
1451
|
+
});
|
|
1452
|
+
};
|
|
1453
|
+
// wrapper fixed bottom
|
|
1454
|
+
const wrapper = document.createElement('div');
|
|
1455
|
+
wrapper.className = 'sovads-bottom-bar';
|
|
1456
|
+
wrapper.style.cssText = `
|
|
1457
|
+
position: fixed;
|
|
1458
|
+
left: 0;
|
|
1459
|
+
bottom: 0;
|
|
1460
|
+
width: 100%;
|
|
1461
|
+
z-index: 10000;
|
|
1462
|
+
display: flex;
|
|
1463
|
+
justify-content: center;
|
|
1464
|
+
background: rgba(255,255,255,0.95);
|
|
1465
|
+
box-shadow: 0 -2px 6px rgba(0,0,0,0.2);
|
|
1466
|
+
`;
|
|
1467
|
+
const bar = document.createElement('div');
|
|
1468
|
+
bar.style.cssText = `
|
|
1469
|
+
max-width: 720px;
|
|
1470
|
+
width: 100%;
|
|
1471
|
+
position: relative;
|
|
1472
|
+
padding: 8px;
|
|
1473
|
+
cursor: pointer;
|
|
1474
|
+
`;
|
|
1475
|
+
// close button
|
|
1476
|
+
const closeBtn = document.createElement('button');
|
|
1477
|
+
closeBtn.innerHTML = '×';
|
|
1478
|
+
closeBtn.style.cssText = `
|
|
1479
|
+
position: absolute;
|
|
1480
|
+
right: 8px;
|
|
1481
|
+
top: 8px;
|
|
1482
|
+
background: none;
|
|
1483
|
+
border: none;
|
|
1484
|
+
font-size: 20px;
|
|
1485
|
+
cursor: pointer;
|
|
1486
|
+
color: #666;
|
|
1487
|
+
`;
|
|
1488
|
+
closeBtn.addEventListener('click', (e) => {
|
|
1489
|
+
e.stopPropagation();
|
|
1490
|
+
this.hide();
|
|
1491
|
+
});
|
|
1492
|
+
// create media element
|
|
1493
|
+
const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
|
|
1494
|
+
let mediaEl;
|
|
1495
|
+
if (mediaType === 'video') {
|
|
1496
|
+
const video = document.createElement('video');
|
|
1497
|
+
video.src = this.currentAd.bannerUrl;
|
|
1498
|
+
video.muted = true;
|
|
1499
|
+
video.autoplay = true;
|
|
1500
|
+
video.loop = true;
|
|
1501
|
+
video.playsInline = true;
|
|
1502
|
+
video.controls = true;
|
|
1503
|
+
video.style.cssText = 'width:100%;height:auto;';
|
|
1504
|
+
video.addEventListener('loadeddata', () => {
|
|
1505
|
+
const rt = Date.now() - renderStart;
|
|
1506
|
+
trackImp(true, rt);
|
|
1507
|
+
}, { once: true });
|
|
1508
|
+
video.addEventListener('error', () => {
|
|
1509
|
+
const rt = Date.now() - renderStart;
|
|
1510
|
+
trackImp(false, rt);
|
|
1511
|
+
}, { once: true });
|
|
1512
|
+
mediaEl = video;
|
|
1513
|
+
}
|
|
1514
|
+
else {
|
|
1515
|
+
const img = document.createElement('img');
|
|
1516
|
+
img.src = this.currentAd.bannerUrl;
|
|
1517
|
+
img.alt = this.currentAd.description;
|
|
1518
|
+
img.style.cssText = 'width:100%;height:auto;';
|
|
1519
|
+
img.addEventListener('load', () => {
|
|
1520
|
+
const rt = Date.now() - renderStart;
|
|
1521
|
+
trackImp(true, rt);
|
|
1522
|
+
});
|
|
1523
|
+
img.addEventListener('error', () => {
|
|
1524
|
+
const rt = Date.now() - renderStart;
|
|
1525
|
+
trackImp(false, rt);
|
|
1526
|
+
});
|
|
1527
|
+
mediaEl = img;
|
|
1528
|
+
}
|
|
1529
|
+
const handleClick = () => {
|
|
1530
|
+
if (!this.currentAd)
|
|
1531
|
+
return;
|
|
1532
|
+
this.sovads._trackEvent('CLICK', this.currentAd.id, this.currentAd.campaignId, {
|
|
1533
|
+
rendered: true,
|
|
1534
|
+
viewportVisible: true,
|
|
1535
|
+
renderTime: Date.now() - renderStart,
|
|
1536
|
+
});
|
|
1537
|
+
window.open(this.sovads.normalizeUrl(this.currentAd.targetUrl), '_blank', 'noopener,noreferrer');
|
|
1538
|
+
this.hide();
|
|
1539
|
+
};
|
|
1540
|
+
bar.appendChild(closeBtn);
|
|
1541
|
+
bar.appendChild(mediaEl);
|
|
1542
|
+
bar.addEventListener('click', handleClick);
|
|
1543
|
+
wrapper.appendChild(bar);
|
|
1544
|
+
document.body.appendChild(wrapper);
|
|
1545
|
+
this.barElement = wrapper;
|
|
1546
|
+
}
|
|
1547
|
+
hide() {
|
|
1548
|
+
if (this.barElement && this.barElement.isConnected) {
|
|
1549
|
+
this.barElement.remove();
|
|
1550
|
+
}
|
|
1551
|
+
this.barElement = null;
|
|
1552
|
+
this.currentAd = null;
|
|
1553
|
+
this.isVisible = false;
|
|
941
1554
|
}
|
|
942
1555
|
}
|
|
943
1556
|
// Sidebar Component
|
|
944
1557
|
export class Sidebar {
|
|
945
|
-
constructor(sovads, containerId) {
|
|
1558
|
+
constructor(sovads, containerId, slotConfig = {}) {
|
|
946
1559
|
this.currentAd = null;
|
|
947
1560
|
this.renderStartTime = 0;
|
|
948
1561
|
this.hasTrackedImpression = false;
|
|
949
1562
|
this.isRendering = false;
|
|
1563
|
+
this.refreshTimer = null;
|
|
1564
|
+
this.lastAdId = null;
|
|
1565
|
+
this.retryCount = 0;
|
|
1566
|
+
this.maxRetries = 3;
|
|
950
1567
|
this.sovads = sovads;
|
|
951
1568
|
this.containerId = containerId;
|
|
1569
|
+
this.slotConfig = slotConfig;
|
|
952
1570
|
}
|
|
953
|
-
async render(consumerId) {
|
|
1571
|
+
async render(consumerId, forceRefresh = false) {
|
|
954
1572
|
// Prevent concurrent renders
|
|
955
|
-
if (this.isRendering) {
|
|
1573
|
+
if (this.isRendering && !forceRefresh) {
|
|
956
1574
|
if (this.sovads.getConfig().debug) {
|
|
957
1575
|
console.warn(`Sidebar render already in progress for ${this.containerId}`);
|
|
958
1576
|
}
|
|
@@ -966,8 +1584,32 @@ export class Sidebar {
|
|
|
966
1584
|
this.isRendering = false;
|
|
967
1585
|
return;
|
|
968
1586
|
}
|
|
1587
|
+
// Lazy loading: wait for container to be in viewport
|
|
1588
|
+
if (this.sovads.getConfig().lazyLoad && !forceRefresh) {
|
|
1589
|
+
const isInViewport = await this.checkViewport(container);
|
|
1590
|
+
if (!isInViewport) {
|
|
1591
|
+
this.setupLazyLoadObserver(container, consumerId);
|
|
1592
|
+
this.isRendering = false;
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
969
1596
|
this.renderStartTime = Date.now();
|
|
970
|
-
this.currentAd = await this.sovads.loadAd(
|
|
1597
|
+
this.currentAd = await this.sovads.loadAd({
|
|
1598
|
+
consumerId,
|
|
1599
|
+
placement: this.slotConfig.placementId || 'sidebar',
|
|
1600
|
+
size: this.slotConfig.size,
|
|
1601
|
+
});
|
|
1602
|
+
this.hasTrackedImpression = false;
|
|
1603
|
+
// Skip if same ad (rotation disabled or same ad returned)
|
|
1604
|
+
if (!forceRefresh && this.lastAdId === this.currentAd?.id && this.sovads.getConfig().rotationEnabled) {
|
|
1605
|
+
if (this.sovads.getConfig().debug) {
|
|
1606
|
+
console.log('Same ad returned, skipping render');
|
|
1607
|
+
}
|
|
1608
|
+
this.isRendering = false;
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
this.lastAdId = this.currentAd?.id || null;
|
|
1612
|
+
this.retryCount = 0;
|
|
971
1613
|
if (!this.currentAd) {
|
|
972
1614
|
container.innerHTML = '<div class="sovads-no-ad">No ads available</div>';
|
|
973
1615
|
this.isRendering = false;
|
|
@@ -975,6 +1617,7 @@ export class Sidebar {
|
|
|
975
1617
|
}
|
|
976
1618
|
// Handle dummy ads for unregistered sites
|
|
977
1619
|
if (this.currentAd.isDummy) {
|
|
1620
|
+
container.innerHTML = '';
|
|
978
1621
|
const dummyElement = document.createElement('div');
|
|
979
1622
|
dummyElement.className = 'sovads-sidebar-dummy';
|
|
980
1623
|
dummyElement.setAttribute('data-ad-id', this.currentAd.id);
|
|
@@ -1021,18 +1664,20 @@ export class Sidebar {
|
|
|
1021
1664
|
return;
|
|
1022
1665
|
}
|
|
1023
1666
|
const adElement = document.createElement('div');
|
|
1667
|
+
container.innerHTML = '';
|
|
1024
1668
|
adElement.className = 'sovads-sidebar';
|
|
1025
1669
|
adElement.setAttribute('data-ad-id', this.currentAd.id);
|
|
1670
|
+
const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
|
|
1026
1671
|
adElement.style.cssText = `
|
|
1027
1672
|
background: #f8f9fa;
|
|
1028
1673
|
border: 1px solid #e9ecef;
|
|
1029
1674
|
border-radius: 8px;
|
|
1030
1675
|
padding: 15px;
|
|
1031
1676
|
margin-bottom: 15px;
|
|
1032
|
-
cursor: pointer;
|
|
1677
|
+
cursor: ${mediaType === 'video' ? 'default' : 'pointer'};
|
|
1033
1678
|
transition: all 0.2s ease;
|
|
1679
|
+
opacity: 0;
|
|
1034
1680
|
`;
|
|
1035
|
-
const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
|
|
1036
1681
|
const handleVisibilityTracking = (renderInfo) => {
|
|
1037
1682
|
this.sovads.setupRenderObserver(adElement, this.currentAd.id, (isVisible) => {
|
|
1038
1683
|
renderInfo.viewportVisible = isVisible;
|
|
@@ -1043,6 +1688,7 @@ export class Sidebar {
|
|
|
1043
1688
|
});
|
|
1044
1689
|
};
|
|
1045
1690
|
const handleRenderSuccess = () => {
|
|
1691
|
+
adElement.style.opacity = '1';
|
|
1046
1692
|
const renderTime = Date.now() - this.renderStartTime;
|
|
1047
1693
|
handleVisibilityTracking({
|
|
1048
1694
|
rendered: true,
|
|
@@ -1051,6 +1697,7 @@ export class Sidebar {
|
|
|
1051
1697
|
});
|
|
1052
1698
|
};
|
|
1053
1699
|
const handleRenderError = () => {
|
|
1700
|
+
adElement.style.opacity = '1';
|
|
1054
1701
|
if (this.sovads.getConfig().debug) {
|
|
1055
1702
|
console.warn(`Failed to load sidebar ad media: ${this.currentAd.bannerUrl}`);
|
|
1056
1703
|
}
|
|
@@ -1083,14 +1730,12 @@ export class Sidebar {
|
|
|
1083
1730
|
img.addEventListener('error', handleRenderError, { once: true });
|
|
1084
1731
|
mediaElement = img;
|
|
1085
1732
|
}
|
|
1086
|
-
|
|
1087
|
-
adElement.addEventListener('click', () => {
|
|
1733
|
+
const handleClickThrough = () => {
|
|
1088
1734
|
this.sovads._trackEvent('CLICK', this.currentAd.id, this.currentAd.campaignId, {
|
|
1089
1735
|
rendered: true,
|
|
1090
1736
|
viewportVisible: true,
|
|
1091
1737
|
renderTime: Date.now() - this.renderStartTime
|
|
1092
1738
|
});
|
|
1093
|
-
// Log interaction
|
|
1094
1739
|
this.sovads.logInteraction('CLICK', {
|
|
1095
1740
|
adId: this.currentAd.id,
|
|
1096
1741
|
campaignId: this.currentAd.campaignId,
|
|
@@ -1098,7 +1743,7 @@ export class Sidebar {
|
|
|
1098
1743
|
metadata: { renderTime: Date.now() - this.renderStartTime },
|
|
1099
1744
|
});
|
|
1100
1745
|
window.open(this.sovads.normalizeUrl(this.currentAd.targetUrl), '_blank', 'noopener,noreferrer');
|
|
1101
|
-
}
|
|
1746
|
+
};
|
|
1102
1747
|
// Add hover effect
|
|
1103
1748
|
adElement.addEventListener('mouseenter', () => {
|
|
1104
1749
|
adElement.style.background = '#e9ecef';
|
|
@@ -1108,17 +1753,116 @@ export class Sidebar {
|
|
|
1108
1753
|
adElement.style.background = '#f8f9fa';
|
|
1109
1754
|
adElement.style.transform = 'translateY(0)';
|
|
1110
1755
|
});
|
|
1111
|
-
mediaElement.style.cursor = 'pointer';
|
|
1112
|
-
|
|
1756
|
+
mediaElement.style.cursor = mediaType === 'video' ? 'default' : 'pointer';
|
|
1757
|
+
if (mediaType === 'video') {
|
|
1758
|
+
const ctaButton = document.createElement('button');
|
|
1759
|
+
ctaButton.type = 'button';
|
|
1760
|
+
ctaButton.textContent = 'Learn more';
|
|
1761
|
+
ctaButton.style.cssText = `
|
|
1762
|
+
width: 100%;
|
|
1763
|
+
border: none;
|
|
1764
|
+
margin-top: 8px;
|
|
1765
|
+
background: #111;
|
|
1766
|
+
color: #fff;
|
|
1767
|
+
font-size: 12px;
|
|
1768
|
+
font-weight: 600;
|
|
1769
|
+
padding: 8px 12px;
|
|
1770
|
+
border-radius: 6px;
|
|
1771
|
+
cursor: pointer;
|
|
1772
|
+
`;
|
|
1773
|
+
ctaButton.addEventListener('click', handleClickThrough);
|
|
1774
|
+
adElement.appendChild(mediaElement);
|
|
1775
|
+
adElement.appendChild(ctaButton);
|
|
1776
|
+
}
|
|
1777
|
+
else {
|
|
1778
|
+
adElement.addEventListener('click', handleClickThrough);
|
|
1779
|
+
adElement.appendChild(mediaElement);
|
|
1780
|
+
}
|
|
1113
1781
|
container.appendChild(adElement);
|
|
1782
|
+
// Set up auto-refresh if enabled
|
|
1783
|
+
this.setupAutoRefresh(consumerId);
|
|
1784
|
+
}
|
|
1785
|
+
catch (error) {
|
|
1786
|
+
// Retry logic on error
|
|
1787
|
+
if (this.retryCount < this.maxRetries) {
|
|
1788
|
+
this.retryCount++;
|
|
1789
|
+
if (this.sovads.getConfig().debug) {
|
|
1790
|
+
console.warn(`Sidebar render failed, retrying (${this.retryCount}/${this.maxRetries})...`);
|
|
1791
|
+
}
|
|
1792
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * this.retryCount));
|
|
1793
|
+
this.isRendering = false;
|
|
1794
|
+
return this.render(consumerId, true);
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
const container = document.getElementById(this.containerId);
|
|
1798
|
+
if (container) {
|
|
1799
|
+
container.innerHTML = '<div class="sovads-error" style="padding: 10px; text-align: center; color: #666; font-size: 12px;">Ad temporarily unavailable</div>';
|
|
1800
|
+
}
|
|
1801
|
+
if (this.sovads.getConfig().debug) {
|
|
1802
|
+
console.error('Sidebar render failed after retries:', error);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1114
1805
|
}
|
|
1115
1806
|
finally {
|
|
1116
1807
|
this.isRendering = false;
|
|
1117
1808
|
}
|
|
1118
1809
|
}
|
|
1810
|
+
async checkViewport(element) {
|
|
1811
|
+
return new Promise((resolve) => {
|
|
1812
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
1813
|
+
resolve(true);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
const observer = new IntersectionObserver((entries) => {
|
|
1817
|
+
entries.forEach((entry) => {
|
|
1818
|
+
if (entry.isIntersecting) {
|
|
1819
|
+
observer.disconnect();
|
|
1820
|
+
resolve(true);
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
}, { rootMargin: '50px' });
|
|
1824
|
+
observer.observe(element);
|
|
1825
|
+
setTimeout(() => {
|
|
1826
|
+
observer.disconnect();
|
|
1827
|
+
resolve(true);
|
|
1828
|
+
}, 5000);
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
setupLazyLoadObserver(container, consumerId) {
|
|
1832
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
1833
|
+
this.render(consumerId);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
const observer = new IntersectionObserver((entries) => {
|
|
1837
|
+
entries.forEach((entry) => {
|
|
1838
|
+
if (entry.isIntersecting && !this.isRendering) {
|
|
1839
|
+
observer.disconnect();
|
|
1840
|
+
this.render(consumerId);
|
|
1841
|
+
}
|
|
1842
|
+
});
|
|
1843
|
+
}, { rootMargin: '50px' });
|
|
1844
|
+
observer.observe(container);
|
|
1845
|
+
}
|
|
1846
|
+
setupAutoRefresh(consumerId) {
|
|
1847
|
+
if (this.refreshTimer) {
|
|
1848
|
+
clearInterval(this.refreshTimer);
|
|
1849
|
+
}
|
|
1850
|
+
const refreshInterval = this.sovads.getConfig().refreshInterval || 0;
|
|
1851
|
+
if (refreshInterval > 0) {
|
|
1852
|
+
this.refreshTimer = window.setInterval(() => {
|
|
1853
|
+
if (!this.isRendering) {
|
|
1854
|
+
this.render(consumerId, true);
|
|
1855
|
+
}
|
|
1856
|
+
}, refreshInterval * 1000);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
destroy() {
|
|
1860
|
+
if (this.refreshTimer) {
|
|
1861
|
+
clearInterval(this.refreshTimer);
|
|
1862
|
+
this.refreshTimer = null;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1119
1865
|
}
|
|
1120
|
-
// Export main SovAds class
|
|
1121
|
-
export { SovAds };
|
|
1122
1866
|
// Default export for easy importing
|
|
1123
1867
|
export default SovAds;
|
|
1124
1868
|
//# sourceMappingURL=index.js.map
|