UNPKG

c3-sdk

Version:

A lightweight JavaScript SDK for Google AdSense, Google Publisher Tag (GPT), and Google AdSense for Search (AFS) integration

1,597 lines 52.3 kB
var __defProp = Object.defineProperty; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; function loadScript(src, options = {}) { return new Promise((resolve, reject) => { const existingScript = document.querySelector(`script[src="${src}"]`); if (existingScript) { resolve(); return; } const script = document.createElement("script"); script.src = src; script.async = options.async; if (options.pubId) { script.setAttribute("data-ad-client", options.pubId); script.setAttribute("data-ad-frequency-hint", options.hint); } if (options.crossOrigin) { script.setAttribute("crossorigin", options.crossOrigin); } script.onload = () => { resolve(); }; script.onerror = () => { reject(new Error(`Script load failed: ${src}`)); }; document.head.appendChild(script); }); } function lazyLoadAd(element, loadCallback, options = {}) { const { rootMargin = "50px" } = options; if (!window.IntersectionObserver) { loadCallback(); return null; } const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { loadCallback(); observer.unobserve(element); } }); }, { rootMargin, threshold: 0.01 } ); observer.observe(element); return observer; } function setAutoRefresh(refreshCallback, intervalSeconds) { if (intervalSeconds <= 0) { return null; } const intervalMs = intervalSeconds * 1e3; return setInterval(() => { try { refreshCallback(); } catch (error) { console.error("Ad auto refresh failed:", error); } }, intervalMs); } function clearAutoRefresh(timerId) { if (timerId) { clearInterval(timerId); } } const AdSense = { /** * Initialize AdSense * @param {Object} config - Config object */ init(config2) { this.config = config2; this.pubId = config2.pubId; this.ads = []; this.hashChangeHandler = null; this.adCounter = 0; window.adsbygoogle = window.adsbygoogle || []; const vignetteConfig = config2.adsenseConfig && config2.adsenseConfig.vignetteConfig || config2.vignetteConfig; if (vignetteConfig && vignetteConfig.enabled) { this.setupHashChangeListener(); } if (vignetteConfig && vignetteConfig.initialPrerollDelay > 0) { this.setupInitialPreroll(); } const rewardConfig = config2.adsenseConfig && config2.adsenseConfig.rewardConfig || {}; if (rewardConfig.initialRewardDelay > 0) { this.setupInitialReward(); } }, /** * Setup hashchange listener for vignette/preroll detection */ setupHashChangeListener() { const vignetteConfig = this.getVignetteConfig(); if (!vignetteConfig || !vignetteConfig.enabled) { return; } this.initAdTracking(); this.hashChangeHandler = () => { this.handleHashChange(); }; window.addEventListener("hashchange", this.hashChangeHandler); if (vignetteConfig.maxVignetteMissed > 0) { this.vignetteCheckInterval = setInterval(() => { this.checkMissedVignette(); }, 3e4); } this.handleHashChange(); this.initCycleState(); }, /** * Get vignette/preroll config */ getVignetteConfig() { return this.config.adsenseConfig && this.config.adsenseConfig.vignetteConfig || this.config.vignetteConfig; }, /** * Check for missed vignettes */ checkMissedVignette() { const config2 = this.getVignetteConfig(); if (!config2 || !config2.enabled || config2.maxVignetteMissed <= 0) { return; } const tracking = this.getAdTracking(); if (!tracking) { return; } const now = Date.now(); const timeSinceLastVignette = now - (tracking.lastVignetteTime || 0); if (timeSinceLastVignette > 6e4 && tracking.lastVignetteTime > 0) { const newMissedCount = (tracking.missedVignetteCount || 0) + 1; this.updateAdTracking({ missedVignetteCount: newMissedCount }); if (newMissedCount >= config2.maxVignetteMissed) { this._internalShowPreroll(); } } }, /** * Initialize ad tracking data */ initAdTracking() { const storageKey = "c3_adsense_ad_tracking"; const data = sessionStorage.getItem(storageKey); if (!data) { const initialData = { vignetteCount: 0, // Current cycle vignette count prerollCount: 0, // Current cycle preroll count lastVignetteTime: 0, // Last vignette time missedVignetteCount: 0, // Missed vignette count totalVignetteCount: 0, // Total vignette count totalPrerollCount: 0, // Total preroll count currentCycle: "vignette" // Current cycle: "vignette" or "preroll" }; sessionStorage.setItem(storageKey, JSON.stringify(initialData)); } }, /** * Initialize cycle state */ initCycleState() { const tracking = this.getAdTracking(); if (tracking && !tracking.currentCycle) { this.updateAdTracking({ currentCycle: "vignette" // Start with vignette }); } }, /** * Get ad tracking data */ getAdTracking() { const storageKey = "c3_adsense_ad_tracking"; const data = sessionStorage.getItem(storageKey); return data ? JSON.parse(data) : null; }, /** * Update ad tracking data */ updateAdTracking(updates) { const storageKey = "c3_adsense_ad_tracking"; const currentData = this.getAdTracking() || { vignetteCount: 0, prerollCount: 0, lastVignetteTime: 0, missedVignetteCount: 0, totalVignetteCount: 0, totalPrerollCount: 0, currentCycle: "vignette" }; const newData = __spreadValues(__spreadValues({}, currentData), updates); sessionStorage.setItem(storageKey, JSON.stringify(newData)); return newData; }, /** * Handle hashchange event */ handleHashChange() { const hash = window.location.hash; const config2 = this.getVignetteConfig(); if (!config2 || !config2.enabled) { return; } let tracking = this.getAdTracking(); if (hash.includes("google_vignette")) { const newVignetteCount = (tracking.vignetteCount || 0) + 1; const newTotalVignetteCount = (tracking.totalVignetteCount || 0) + 1; tracking = this.updateAdTracking({ vignetteCount: newVignetteCount, totalVignetteCount: newTotalVignetteCount, lastVignetteTime: Date.now(), missedVignetteCount: 0, // Reset missed count currentCycle: "vignette" // Current in vignette cycle }); console.log( `Vignette detected, count: ${newVignetteCount}/${config2.vignetteToPreroll.count}` ); if (newVignetteCount >= config2.vignetteToPreroll.count && config2.vignetteToPreroll.count > 0) { tracking = this.updateAdTracking({ vignetteCount: 0, prerollCount: 0, // Reset preroll count currentCycle: "preroll" // Switch to preroll cycle }); const triggerCount = config2.vignetteToPreroll.trigger || 1; console.log( `Vignette threshold reached, triggering ${triggerCount} preroll(s)` ); setTimeout(() => { this.triggerPrerollCycle(triggerCount, 0); }, 100); } } else if (hash.includes("goog_fullscreen_ad")) { const newPrerollCount = (tracking.prerollCount || 0) + 1; const newTotalPrerollCount = (tracking.totalPrerollCount || 0) + 1; tracking = this.updateAdTracking({ prerollCount: newPrerollCount, totalPrerollCount: newTotalPrerollCount, missedVignetteCount: 0, // Reset missed count currentCycle: "preroll" // Current in preroll cycle }); console.log( `Preroll detected, count: ${newPrerollCount}/${config2.prerollToVignette.count}` ); if (newPrerollCount >= config2.prerollToVignette.count && config2.prerollToVignette.count > 0) { tracking = this.updateAdTracking({ vignetteCount: 0, // Reset vignette count prerollCount: 0, currentCycle: "vignette" // Switch back to vignette cycle }); console.log( `Preroll threshold reached, entering vignette cycle (${config2.prerollToVignette.trigger} vignettes)` ); } } else if (hash && hash !== window.location.hash) { const newMissedCount = (tracking.missedVignetteCount || 0) + 1; tracking = this.updateAdTracking({ missedVignetteCount: newMissedCount }); if (newMissedCount >= config2.maxVignetteMissed && config2.maxVignetteMissed > 0 && tracking.currentCycle === "vignette") { setTimeout(() => { this._internalShowPreroll(); }, 100); } } }, /** * Check and trigger preroll * Decide whether to show preroll based on config rules */ checkAndTriggerPreroll() { const config2 = this.getVignetteConfig(); if (!config2 || !config2.enabled) { return false; } const tracking = this.getAdTracking(); if (!tracking) { return false; } const shouldShowPreroll = tracking.vignetteCount >= config2.vignetteToPreroll.count && config2.vignetteToPreroll.count > 0 && tracking.currentCycle === "vignette"; const shouldShowPrerollForMissed = tracking.missedVignetteCount >= config2.maxVignetteMissed && config2.maxVignetteMissed > 0 && tracking.currentCycle === "vignette"; if (shouldShowPreroll) { const triggerCount = config2.vignetteToPreroll.trigger || 1; this.triggerPrerollCycle(triggerCount, 0); return true; } if (shouldShowPrerollForMissed) { this._internalShowPreroll(); return true; } return false; }, /** * Check if AdSense interstitial API is ready * @returns {boolean} True if ready */ _isInterstitialAPIReady() { if (!window.adsbygoogle) { return false; } if (typeof window.adsbygoogle !== "object") { return false; } const scriptLoaded = document.querySelector( 'script[src*="adsbygoogle.js"]' ); if (!scriptLoaded) { return false; } return true; }, /** * Show preroll with retry logic * @param {Function} showFn - Function to show preroll * @param {number} retryCount - Current retry count * @param {number} maxRetries - Maximum retries */ _showPrerollWithRetry(showFn, retryCount = 0, maxRetries = 5) { if (retryCount >= maxRetries) { console.error( "AdSense interstitial API not available after max retries" ); return; } if (!this._isInterstitialAPIReady()) { const delay = retryCount < 2 ? 500 : 1e3; setTimeout(() => { this._showPrerollWithRetry(showFn, retryCount + 1, maxRetries); }, delay); return; } try { showFn(); } catch (e) { if (e.message && (e.message.includes("no interstitial API") || e.message.includes("interstitial"))) { const delay = retryCount < 2 ? 1e3 : 2e3; setTimeout(() => { this._showPrerollWithRetry( showFn, retryCount + 1, maxRetries ); }, delay); } else { console.error("Preroll trigger failed:", e); } } }, /** * Internal method to show preroll ad (for automatic triggers) */ _internalShowPreroll() { const showFn = () => { if (window.adBreak) { window.adBreak({ type: "preroll", adBreakDone: () => { console.log("Preroll completed"); const tracking = this.getAdTracking(); const newPrerollCount = ((tracking == null ? void 0 : tracking.prerollCount) || 0) + 1; const newTotalPrerollCount = ((tracking == null ? void 0 : tracking.totalPrerollCount) || 0) + 1; this.updateAdTracking({ prerollCount: newPrerollCount, totalPrerollCount: newTotalPrerollCount, missedVignetteCount: 0, // Reset missed count currentCycle: "preroll" }); const config2 = this.getVignetteConfig(); if (config2 && newPrerollCount >= config2.prerollToVignette.count && config2.prerollToVignette.count > 0) { this.updateAdTracking({ vignetteCount: 0, prerollCount: 0, currentCycle: "vignette" }); console.log( "Preroll cycle done, entering vignette cycle" ); } } }); console.log("Preroll triggered"); } else { console.error("window.adBreak is not available"); } }; this._showPrerollWithRetry(showFn); }, /** * Manually show preroll ad * @param {Object} options - Preroll ad options * @param {Function} options.beforeAd - Callback before ad shows * @param {Function} options.adDismissed - Callback when ad dismissed * @param {Function} options.adViewed - Callback when ad viewed * @param {Function} options.afterAd - Callback after ad (only if ad shown) * @param {Function} options.adBreakDone - Callback when ad break done (always called) */ showPreroll(options = {}) { const beforeAd = options.beforeAd; const adDismissed = options.adDismissed; const adViewed = options.adViewed; const afterAd = options.afterAd; const adBreakDone = options.adBreakDone; const showFn = () => { if (window.adBreak) { const prerollOption = { type: "preroll", beforeAd: () => { console.log("Preroll ad - beforeAd"); if (beforeAd) { beforeAd(); } }, adDismissed: () => { console.log("Preroll ad - adDismissed"); if (adDismissed) { adDismissed(); } }, adViewed: () => { console.log("Preroll ad - adViewed"); if (adViewed) { adViewed(); } }, afterAd: () => { console.log("Preroll ad - afterAd (only if ad shown)"); if (afterAd) { afterAd(); } }, adBreakDone: () => { console.log("Preroll ad - adBreakDone (always called)"); const tracking = this.getAdTracking(); const newPrerollCount = ((tracking == null ? void 0 : tracking.prerollCount) || 0) + 1; const newTotalPrerollCount = ((tracking == null ? void 0 : tracking.totalPrerollCount) || 0) + 1; this.updateAdTracking({ prerollCount: newPrerollCount, totalPrerollCount: newTotalPrerollCount, missedVignetteCount: 0, currentCycle: "preroll" }); const config2 = this.getVignetteConfig(); if (config2 && newPrerollCount >= config2.prerollToVignette.count && config2.prerollToVignette.count > 0) { this.updateAdTracking({ vignetteCount: 0, prerollCount: 0, currentCycle: "vignette" }); console.log( "Preroll cycle done, entering vignette cycle" ); } if (adBreakDone) { adBreakDone(); } } }; window.adBreak(prerollOption); console.log("Preroll ad triggered"); } else { console.error("window.adBreak is not available"); } }; this._showPrerollWithRetry(showFn); }, /** * Trigger preroll cycle (by count) * @param {number} count - Number of prerolls to trigger * @param {number} current - Current triggered count */ triggerPrerollCycle(count, current) { if (current >= count) { console.log(`Preroll cycle completed: ${count} triggered`); return; } const tracking = this.getAdTracking(); const beforePrerollCount = (tracking == null ? void 0 : tracking.prerollCount) || 0; this._internalShowPreroll(); const checkInterval = setInterval(() => { const newTracking = this.getAdTracking(); const newPrerollCount = (newTracking == null ? void 0 : newTracking.prerollCount) || 0; if (newPrerollCount > beforePrerollCount) { clearInterval(checkInterval); setTimeout(() => { this.triggerPrerollCycle(count, newPrerollCount); }, 500); } }, 500); setTimeout(() => { clearInterval(checkInterval); const newTracking = this.getAdTracking(); const newPrerollCount = (newTracking == null ? void 0 : newTracking.prerollCount) || 0; if (newPrerollCount === beforePrerollCount) { console.warn("Preroll may not completed, continuing"); this.triggerPrerollCycle(count, current + 1); } else { this.triggerPrerollCycle(count, newPrerollCount); } }, 3e4); }, /** * Setup initial preroll (delayed trigger after page load, one-time only) */ setupInitialPreroll() { const config2 = this.getVignetteConfig(); if (!config2 || config2.initialPrerollDelay <= 0) { return; } const storageKey = "c3_adsense_initial_preroll_triggered"; const hasTriggered = sessionStorage.getItem(storageKey); if (hasTriggered === "true") { console.log("Initial preroll already triggered, skipping"); return; } this.initialPrerollTimer = setTimeout(() => { if (sessionStorage.getItem(storageKey) === "true") { return; } sessionStorage.setItem(storageKey, "true"); console.log( `Initial preroll triggered after ${config2.initialPrerollDelay}s` ); this._internalShowPreroll(); }, config2.initialPrerollDelay * 1e3); }, /** * Setup initial reward (delayed trigger after page load, one-time only) */ setupInitialReward() { const config2 = this.config.adsenseConfig || {}; const rewardConfig = config2.rewardConfig || {}; if (!rewardConfig || rewardConfig.initialRewardDelay <= 0) { return; } const storageKey = "c3_adsense_initial_reward_triggered"; const hasTriggered = sessionStorage.getItem(storageKey); if (hasTriggered === "true") { console.log("Initial reward already triggered, skipping"); return; } this.initialRewardTimer = setTimeout(() => { if (sessionStorage.getItem(storageKey) === "true") { return; } sessionStorage.setItem(storageKey, "true"); console.log( `Initial reward triggered after ${rewardConfig.initialRewardDelay}s` ); this.showReward(); }, rewardConfig.initialRewardDelay * 1e3); }, /** * Cleanup hashchange listener */ cleanup() { if (this.hashChangeHandler) { window.removeEventListener("hashchange", this.hashChangeHandler); this.hashChangeHandler = null; } if (this.vignetteCheckInterval) { clearInterval(this.vignetteCheckInterval); this.vignetteCheckInterval = null; } if (this.initialPrerollTimer) { clearTimeout(this.initialPrerollTimer); this.initialPrerollTimer = null; } if (this.initialRewardTimer) { clearTimeout(this.initialRewardTimer); this.initialRewardTimer = null; } }, /** * Create ad unit * @param {Object} options - Ad options * @param {string} options.adSlotId - Ad slot ID * @param {string} options.adFormat - Ad format (auto, responsive, rectangle, etc.) * @param {boolean} options.fullWidthResponsive - Full width responsive * @param {string} options.containerId - Container ID * @param {boolean} options.lazyLoad - Enable lazy load (default: false) * @param {number} options.autoRefreshSeconds - Auto refresh seconds (0 = disabled) * @param {number|string} options.width - Ad width (number or string, e.g., 300 or "300px") * @param {number|string} options.height - Ad height (number or string, e.g., 250 or "250px") * @param {string} options.display - Display type ("block", "inline-block", "inline"), default "inline-block" * @returns {Object} Ad object */ createAd(options = {}) { if (!this.config) { throw new Error("AdSense not initialized, call c3.init() first"); } const { adSlotId, adFormat = "auto", fullWidthResponsive = true, containerId, lazyLoad = false, autoRefreshSeconds = 0, width, height, display = "inline-block" } = options; if (!adSlotId) { throw new Error("adSlotId is required"); } const container = containerId ? document.getElementById(containerId) : document.body; if (!container) { throw new Error(`Container not found: ${containerId || "body"}`); } const ins = document.createElement("ins"); ins.className = "adsbygoogle"; ins.style.display = display; if (width) { if (typeof width === "number") { ins.style.width = `${width}px`; } else { ins.style.width = width; } } else { ins.style.width = "100%"; } if (height) { if (typeof height === "number") { ins.style.height = `${height}px`; } else { ins.style.height = height; } } ins.setAttribute("data-ad-client", this.pubId); ins.setAttribute("data-ad-slot", adSlotId); if (!height) { ins.setAttribute("data-full-width-responsive", "true"); ins.setAttribute("data-ad-format", adFormat); } container.appendChild(ins); const adId = `c3_ad_${Date.now()}_${++this.adCounter}_${Math.random().toString(36).substr(2, 9)}`; const createAdElement = () => { const newIns = document.createElement("ins"); newIns.className = "adsbygoogle"; newIns.style.display = display; if (width !== void 0) { if (typeof width === "number") { newIns.style.width = `${width}px`; } else { newIns.style.width = width; } } else { newIns.style.width = "100%"; } if (height !== void 0) { if (typeof height === "number") { newIns.style.height = `${height}px`; } else { newIns.style.height = height; } } newIns.setAttribute("data-ad-client", this.pubId); newIns.setAttribute("data-ad-slot", adSlotId); if (fullWidthResponsive) { newIns.setAttribute("data-full-width-responsive", "true"); newIns.setAttribute("data-ad-format", adFormat); } return newIns; }; const loadAd = () => { try { (window.adsbygoogle = window.adsbygoogle || []).push({}); } catch (e) { console.error("AdSense push failed:", e); } }; const refreshAd = () => { try { const adIndex = this.ads.findIndex((a) => a.id === adId); if (adIndex === -1) { console.warn("Ad not found, cannot refresh"); return; } const currentAd = this.ads[adIndex]; const currentElement = currentAd.element; if (!currentElement || !currentElement.parentNode) { console.warn("Ad element has no parent, cannot refresh"); return; } const parent = currentElement.parentNode; parent.removeChild(currentElement); const newIns = createAdElement(); parent.appendChild(newIns); currentAd.element = newIns; if (currentAd.observer) { currentAd.observer.disconnect(); currentAd.observer = null; } if (lazyLoad) { currentAd.observer = lazyLoadAd(newIns, loadAd, { rootMargin: "50px" }); } else { loadAd(); } } catch (e) { console.error("AdSense refresh failed:", e); } }; let observer = null; if (lazyLoad) { observer = lazyLoadAd(ins, loadAd, { rootMargin: "50px" }); } else { loadAd(); } let refreshTimer = null; if (autoRefreshSeconds > 0) { refreshTimer = setAutoRefresh(refreshAd, autoRefreshSeconds); } const adObject = { id: adId, // Unique ad ID element: ins, adSlotId, containerId: containerId || "body", lazyLoad, autoRefreshSeconds, refreshTimer, observer, // Store ad config for refresh adFormat, fullWidthResponsive, width, height, display, container, refreshAd // Store refresh function }; this.ads.push(adObject); return adObject; }, /** * Preload ads */ /** * Refresh ad * @param {string|Object} identifier - Ad slot ID, ad ID, or ad object */ refresh(identifier) { if (!identifier) { this.ads.forEach((ad) => { if (ad.refreshAd && typeof ad.refreshAd === "function") { ad.refreshAd(); } else { this._recreateAd(ad); } }); } else if (typeof identifier === "object" && identifier.id) { const ad = this.ads.find((a) => a.id === identifier.id); if (ad) { if (ad.refreshAd && typeof ad.refreshAd === "function") { ad.refreshAd(); } else { this._recreateAd(ad); } } } else if (typeof identifier === "string") { if (identifier.startsWith("c3_ad_")) { const ad = this.ads.find((a) => a.id === identifier); if (ad) { if (ad.refreshAd && typeof ad.refreshAd === "function") { ad.refreshAd(); } else { this._recreateAd(ad); } } } else { const matchingAds = this.ads.filter( (a) => a.adSlotId === identifier ); matchingAds.forEach((ad) => { if (ad.refreshAd && typeof ad.refreshAd === "function") { ad.refreshAd(); } else { this._recreateAd(ad); } }); } } }, /** * Recreate ad element (internal helper) * @param {Object} ad - Ad object */ _recreateAd(ad) { if (!ad.element || !ad.element.parentNode) { console.warn("Ad element not found, cannot refresh"); return; } try { const parent = ad.element.parentNode; const container = ad.container || parent; parent.removeChild(ad.element); const newIns = document.createElement("ins"); newIns.className = "adsbygoogle"; newIns.style.display = ad.display || "inline-block"; if (ad.width !== void 0) { if (typeof ad.width === "number") { newIns.style.width = `${ad.width}px`; } else { newIns.style.width = ad.width; } } else { newIns.style.width = "100%"; } if (ad.height !== void 0) { if (typeof ad.height === "number") { newIns.style.height = `${ad.height}px`; } else { newIns.style.height = ad.height; } } newIns.setAttribute("data-ad-client", this.pubId); newIns.setAttribute("data-ad-slot", ad.adSlotId); if (ad.fullWidthResponsive) { newIns.setAttribute("data-full-width-responsive", "true"); newIns.setAttribute("data-ad-format", ad.adFormat || "auto"); } container.appendChild(newIns); ad.element = newIns; if (ad.observer) { ad.observer.disconnect(); ad.observer = null; } if (ad.lazyLoad) { const loadAd = () => { try { (window.adsbygoogle = window.adsbygoogle || []).push( {} ); } catch (e) { console.error("AdSense push failed:", e); } }; ad.observer = lazyLoadAd(newIns, loadAd, { rootMargin: "50px" }); } else { (window.adsbygoogle = window.adsbygoogle || []).push({}); } } catch (e) { console.error("AdSense refresh failed:", e); } }, /** * Remove ad * @param {string} adSlotId - Ad slot ID */ remove(adSlotId) { const index = this.ads.findIndex((a) => a.adSlotId === adSlotId); if (index !== -1) { const ad = this.ads[index]; if (ad.refreshTimer) { clearAutoRefresh(ad.refreshTimer); } if (ad.observer) { ad.observer.disconnect(); } if (ad.element && ad.element.parentNode) { ad.element.parentNode.removeChild(ad.element); } this.ads.splice(index, 1); } }, /** * Manually trigger preroll (alias for showPreroll) * @param {Object} options - Preroll ad options */ triggerPreroll(options = {}) { this.showPreroll(options); }, /** * Manually trigger reward ad * @param {Object} options - Reward ad options * @param {string} options.name - Reward ad name * @param {Function} options.beforeAd - Callback before ad shows * @param {Function} options.beforeReward - Callback before reward (receives showAdFn) * @param {Function} options.adDismissed - Callback when ad dismissed * @param {Function} options.adViewed - Callback when ad viewed * @param {Function} options.afterAd - Callback after ad (only if ad shown) * @param {Function} options.adBreakDone - Callback when ad break done (always called) */ showReward(options = {}) { const config2 = this.config.adsenseConfig || {}; const rewardConfig = config2.rewardConfig || {}; const rewardName = options.name || rewardConfig.name || "c3_reward"; const beforeAd = options.beforeAd || rewardConfig.beforeAd; const beforeReward = options.beforeReward || rewardConfig.beforeReward; const adDismissed = options.adDismissed || rewardConfig.adDismissed; const adViewed = options.adViewed || rewardConfig.adViewed; const afterAd = options.afterAd || rewardConfig.afterAd; const adBreakDone = options.adBreakDone || rewardConfig.adBreakDone; try { if (window.adBreak) { const afgOption = { type: "reward", name: rewardName, beforeAd: () => { console.log("Reward ad - beforeAd"); if (beforeAd) { beforeAd(); } }, beforeReward: (showAdFn) => { console.log("Reward ad - beforeReward"); if (beforeReward) { beforeReward(showAdFn); } else if (showAdFn) { showAdFn(); } }, adDismissed: () => { console.log("Reward ad - adDismissed"); if (adDismissed) { adDismissed(); } }, adViewed: () => { console.log("Reward ad - adViewed"); if (adViewed) { adViewed(); } }, afterAd: () => { console.log("Reward ad - afterAd (only if ad shown)"); if (afterAd) { afterAd(); } }, adBreakDone: () => { console.log("Reward ad - adBreakDone (always called)"); if (adBreakDone) { adBreakDone(); } } }; window.adBreak(afgOption); console.log("Reward ad triggered:", rewardName); } else { console.error("window.adBreak is not available"); } } catch (e) { console.error("Reward ad trigger failed:", e); } }, /** * Get ad tracking stats */ getAdTrackingStats() { return this.getAdTracking(); }, /** * Reset ad tracking data */ resetAdTracking() { const storageKey = "c3_adsense_ad_tracking"; sessionStorage.removeItem(storageKey); this.initAdTracking(); }, /** * Reset initial preroll flag (allow trigger again) */ resetInitialPreroll() { const storageKey = "c3_adsense_initial_preroll_triggered"; sessionStorage.removeItem(storageKey); }, /** * Reset initial reward flag (allow trigger again) */ resetInitialReward() { const storageKey = "c3_adsense_initial_reward_triggered"; sessionStorage.removeItem(storageKey); }, /** * Manually trigger reward ad (alias for showReward) */ triggerReward(options = {}) { this.showReward(options); }, /** * Get all ads */ getAds() { return this.ads; } }; const GPT = { /** * Initialize GPT * @param {Object} config - Config object */ init(config2) { this.config = config2; this.pubId = config2.pubId; this.slots = []; if (!window.googletag) { window.googletag = window.googletag || {}; window.googletag.cmd = window.googletag.cmd || []; } window.googletag.cmd.push(() => { window.googletag.pubads().enableSingleRequest(); window.googletag.pubads().enableAsyncRendering(); window.googletag.pubads().collapseEmptyDivs(); if (config2.pubId) { window.googletag.pubads().setPublisherProvidedId(config2.pubId); } if (config2.nativeAfgSupport) { window.googletag.pubads().enableNativeAds(); } if (config2.gptConfig && typeof config2.gptConfig === "object") { Object.keys(config2.gptConfig).forEach((key) => { if (typeof window.googletag.pubads()[key] === "function") { window.googletag.pubads()[key](config2.gptConfig[key]); } }); } }); }, /** * Define ad slot * @param {Object} options - Ad options * @param {string} options.adUnitPath - Ad unit path (e.g., '/12345678/example') * @param {string} options.size - Ad size (e.g., [300, 250] or [[300, 250], [728, 90]]) * @param {string} options.divId - Container div ID * @param {Object} options.targeting - Targeting parameters * @param {boolean} options.lazyLoad - Enable lazy load (default: false) * @param {number} options.autoRefreshSeconds - Auto refresh seconds (0 = disabled) * @returns {Object} Slot object */ defineSlot(options = {}) { if (!this.config) { throw new Error("GPT not initialized, call c3.init() first"); } const { adUnitPath, size, divId, targeting = {}, lazyLoad = false, autoRefreshSeconds = 0 } = options; if (!adUnitPath || !size || !divId) { throw new Error("adUnitPath, size and divId are required"); } let container = document.getElementById(divId); if (!container) { container = document.createElement("div"); container.id = divId; document.body.appendChild(container); } let slot; const loadAd = () => { window.googletag.cmd.push(() => { slot = window.googletag.defineSlot(adUnitPath, size, divId); if (!slot) { throw new Error("Failed to define slot"); } Object.keys(targeting).forEach((key) => { slot.setTargeting(key, targeting[key]); }); slot.addService(window.googletag.pubads()); window.googletag.display(divId); }); }; const refreshAd = () => { if (slot) { window.googletag.cmd.push(() => { window.googletag.pubads().refresh([slot]); }); } }; let observer = null; if (lazyLoad) { observer = lazyLoadAd(container, loadAd, { rootMargin: "50px" }); } else { loadAd(); } let refreshTimer = null; if (autoRefreshSeconds > 0) { refreshTimer = setAutoRefresh(refreshAd, autoRefreshSeconds); } const slotObject = { slot, adUnitPath, size, divId, targeting, lazyLoad, autoRefreshSeconds, refreshTimer, observer }; this.slots.push(slotObject); return slotObject; }, /** * Refresh slot * @param {string} divId - Container div ID * @param {Object} options - Refresh options */ refresh(divId, options = {}) { const slotObj = this.slots.find((s) => s.divId === divId); if (!slotObj || !slotObj.slot) { throw new Error(`Slot not found: ${divId}`); } window.googletag.cmd.push(() => { window.googletag.pubads().refresh([slotObj.slot], options); }); }, /** * Preload ads */ preload() { if (this.config && this.config.preloadAd) { window.googletag.cmd.push(() => { console.log("GPT preload enabled"); }); } }, /** * Clear slot * @param {string} divId - Container div ID */ clear(divId) { const slotObj = this.slots.find((s) => s.divId === divId); if (slotObj && slotObj.slot) { window.googletag.cmd.push(() => { window.googletag.pubads().clear([slotObj.slot]); }); } }, /** * Remove slot * @param {string} divId - Container div ID */ remove(divId) { const index = this.slots.findIndex((s) => s.divId === divId); if (index !== -1) { const slotObj = this.slots[index]; if (slotObj.refreshTimer) { clearAutoRefresh(slotObj.refreshTimer); } if (slotObj.observer) { slotObj.observer.disconnect(); } if (slotObj.slot) { window.googletag.cmd.push(() => { window.googletag.destroySlots([slotObj.slot]); }); } const container = document.getElementById(divId); if (container) { container.parentNode.removeChild(container); } this.slots.splice(index, 1); } }, /** * Get all slots */ getSlots() { return this.slots; }, /** * Set page-level targeting * @param {string} key - Key * @param {string|string[]} value - Value */ setTargeting(key, value) { window.googletag.cmd.push(() => { window.googletag.pubads().setTargeting(key, value); }); } }; const AFS = { /** * Initialize AFS * @param {Object} config - Config object */ init(config2) { this.config = config2; this.searchBoxes = []; }, /** * Create search box * @param {Object} options - Search box options * @param {string} options.containerId - Container ID * @param {string} options.pubId - Publisher ID (use init pubId if not provided) * @param {string} options.channelId - Channel ID * @param {string} options.placeholder - Placeholder text * @param {string} options.language - Language code (e.g., 'zh_CN') * @param {string} options.searchEngineId - Custom search engine ID * @returns {Object} Search box object */ createSearchBox(options = {}) { if (!this.config) { throw new Error("AFS not initialized, call c3.init() first"); } const { containerId, pubId = this.config.pubId || "", channelId = this.config.channelId || "", placeholder = "Search...", language = "zh_CN", searchEngineId = "" } = options; if (!containerId) { throw new Error("containerId is required"); } let container = document.getElementById(containerId); if (!container) { container = document.createElement("div"); container.id = containerId; document.body.appendChild(container); } const form = document.createElement("form"); form.id = `afs-search-form-${containerId}`; form.method = "get"; form.action = "https://www.google.com/search"; const input = document.createElement("input"); input.type = "text"; input.name = "q"; input.placeholder = placeholder; input.style.width = "100%"; input.style.padding = "8px"; input.style.fontSize = "16px"; const button = document.createElement("button"); button.type = "submit"; button.textContent = "Search"; button.style.padding = "8px 16px"; button.style.marginLeft = "8px"; const hiddenInput = document.createElement("input"); hiddenInput.type = "hidden"; hiddenInput.name = "cx"; hiddenInput.value = searchEngineId || `partner-pub-${pubId}:${channelId}`; const hiddenInput2 = document.createElement("input"); hiddenInput2.type = "hidden"; hiddenInput2.name = "ie"; hiddenInput2.value = "UTF-8"; form.appendChild(input); form.appendChild(button); form.appendChild(hiddenInput); form.appendChild(hiddenInput2); container.appendChild(form); const resultsContainer = document.createElement("div"); resultsContainer.id = `afs-results-${containerId}`; resultsContainer.style.marginTop = "16px"; container.appendChild(resultsContainer); if (searchEngineId) { loadScript(`https://www.google.com/cse?cx=${searchEngineId}`, { async: true }).catch((err) => { console.error("AFS search results script load failed:", err); }); } const searchBoxObject = { containerId, form, input, button, resultsContainer, pubId, channelId, language }; this.searchBoxes.push(searchBoxObject); return searchBoxObject; }, /** * Create AdSense search results ad * @param {Object} options - Ad options * @param {string} options.containerId - Container ID * @param {string} options.adSlotId - Ad slot ID * @param {string} options.adFormat - Ad format * @param {boolean} options.lazyLoad - Enable lazy load (default: false) * @param {number} options.autoRefreshSeconds - Auto refresh seconds (0 = disabled) * @returns {Object} Ad object */ createSearchAd(options = {}) { if (!this.config) { throw new Error("AFS not initialized, call c3.init() first"); } const { containerId, adSlotId, adFormat = "auto", lazyLoad = false, autoRefreshSeconds = 0 } = options; if (!containerId || !adSlotId) { throw new Error("containerId and adSlotId are required"); } const container = document.getElementById(containerId); if (!container) { throw new Error(`Container not found: ${containerId}`); } const ins = document.createElement("ins"); ins.className = "adsbygoogle"; ins.style.display = "block"; ins.setAttribute("data-ad-client", this.config.pubId || ""); ins.setAttribute("data-ad-slot", adSlotId); ins.setAttribute("data-ad-format", adFormat); ins.setAttribute("data-full-width-responsive", "true"); container.appendChild(ins); const loadAd = () => { try { (window.adsbygoogle = window.adsbygoogle || []).push({}); } catch (e) { console.error("AFS ad push failed:", e); } }; const refreshAd = () => { try { (window.adsbygoogle = window.adsbygoogle || []).push({}); } catch (e) { console.error("AFS ad refresh failed:", e); } }; let observer = null; if (lazyLoad) { observer = lazyLoadAd(ins, loadAd, { rootMargin: "50px" }); } else { loadAd(); } let refreshTimer = null; if (autoRefreshSeconds > 0) { refreshTimer = setAutoRefresh(refreshAd, autoRefreshSeconds); } const adObject = { element: ins, adSlotId, containerId, lazyLoad, autoRefreshSeconds, refreshTimer, observer }; const searchBox = this.searchBoxes.find( (box) => box.containerId === containerId ); if (searchBox) { if (!searchBox.ads) { searchBox.ads = []; } searchBox.ads.push(adObject); } return adObject; }, /** * Get all search boxes */ getSearchBoxes() { return this.searchBoxes; }, /** * Remove search box * @param {string} containerId - Container ID */ remove(containerId) { const index = this.searchBoxes.findIndex( (box) => box.containerId === containerId ); if (index !== -1) { const box = this.searchBoxes[index]; if (box.ads && Array.isArray(box.ads)) { box.ads.forEach((ad) => { if (ad.refreshTimer) { clearAutoRefresh(ad.refreshTimer); } if (ad.observer) { ad.observer.disconnect(); } }); } const container = document.getElementById(containerId); if (container) { container.parentNode.removeChild(container); } this.searchBoxes.splice(index, 1); } } }; let config = null; let initialized = false; function init(options = {}) { if (initialized) { console.warn("C3 SDK already initialized"); return Promise.resolve(); } if (!options.platform) { return Promise.reject(new Error("platform is required")); } if (!options.pubId && options.platform !== "afs") { return Promise.reject(new Error("pubId is required")); } config = { platform: options.platform, pubId: options.pubId || "", nativeAfgSupport: options.nativeAfgSupport, channelId: options.channelId || "", useGa: options.useGa || false, useGtm: options.useGtm || false, gaMeasurementId: options.gaMeasurementId || "", gtmContainerId: options.gtmContainerId || "", preloadAd: options.preloadAd, adsenseConfig: __spreadValues({ // Vignette/Preroll config (AdSense only) vignetteConfig: options.adsenseConfig && options.adsenseConfig.vignetteConfig || options.vignetteConfig || { enabled: false, // Enable vignette/preroll management vignetteToPreroll: { // Vignette count before triggering preroll count: 3, // Vignette threshold trigger: 1 // Preroll trigger count }, prerollToVignette: { // Preroll count before triggering vignette count: 1, // Preroll threshold trigger: 3 // Vignette trigger count (AdSense controlled) }, maxVignetteMissed: 2, // Max missed vignettes before preroll (fallback) initialPrerollDelay: 0 // Initial preroll delay in seconds (0=disabled, one-time) }, // Reward ad config rewardConfig: options.adsenseConfig && options.adsenseConfig.rewardConfig || { name: "c3_reward", // Reward ad name initialRewardDelay: 0 // Initial reward delay in seconds (0=disabled, one-time) } }, options.adsenseConfig ? Object.keys(options.adsenseConfig).filter( (key) => key !== "vignetteConfig" && key !== "rewardConfig" ).reduce((acc, key) => { acc[key] = options.adsenseConfig[key]; return acc; }, {}) : {}), gptConfig: options.gptConfig || {}, afsConfig: options.afsConfig || {} }; if (config.useGa) { if (!config.gaMeasurementId) { console.warn("useGa is true but gaMeasurementId is missing"); } else { loadGoogleAnalytics(config.gaMeasurementId); } } if (config.useGtm) { if (!config.gtmContainerId) { console.warn("useGtm is true but gtmContainerId is missing"); } else { loadGoogleTagManager(config.gtmContainerId); } } initialized = true; switch (config.platform) { case "ads": return initializeAdSense(); case "gpt": return initializeGPT(); case "afs": return initializeAFS(); default: return Promise.reject( new Error(`Unsupported platform: ${config.platform}`) ); } } function initializeAdSense() { window.adsbygoogle = window.adsbygoogle || []; let scriptUrl = "https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=" + config.pubId; let scriptOptions = { async: true, crossOrigin: "anonymous" }; if (config.nativeAfgSupport) { scriptUrl = "https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"; scriptOptions = { async: true, hint: "10s", pubId: config.pubId }; } window.adBreak = window.adConfig = function(o) { window.adsbygoogle.push(o); }; if (config.preloadAd) { window.adConfig({ sound: "on", preloadAdBreaks: "on" }); } return loadScript(scriptUrl, scriptOptions).then(() => { AdSense.init(config); console.log("C3 SDK initialized", config); return Promise.resolve(); }).catch((error) => { console.error("AdSense script load failed:", error); return Promise.reject(error); }); } function initializeGPT() { return loadScript("https://securepubads.g.doubleclick.net/tag/js/gpt.js", { async: true }).then(() => { return new Promise((resolve) => { if (window.googletag && window.googletag.apiReady) { GPT.init(config); if (config.preloadAd) { GPT.preload(); } console.log("C3 SDK initialized", config); resolve(); } else { window.googletag = window.googletag || {}; window.googletag.cmd = window.googletag.cmd || []; window.googletag.cmd.push(() => { GPT.init(config); if (config.preloadAd) { GPT.preload(); } console.log("C3 SDK initialized", config); resolve(); }); } }); }).catch((error) => { console.error("GPT script load failed:", error); return Promise.reject(error); }); } function initializeAFS() { return loadScript("https://www.google.com/adsense/search/ads.js", { async: true, crossOrigin: "anonymous" }).then(() => { AFS.init(config); console.log("C3 SDK initialized", config); return Promise.resolve(); }).catch((error) => { console.error("AFS script load failed:", error); return Promise.reject(error); }); } function loadGoogleAnalytics(measurementId) { window.dataLayer = window.dataLayer || []; function gtag() { window.dataLayer.push(arguments); } window.gtag = gtag; gtag("js", /* @__PURE__