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
JavaScript
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__