/** * ROI Theme - AdSense JavaScript-First Visibility Controller * * Mueve las decisiones de visibilidad de anuncios del servidor (PHP) al cliente (JS) * para permitir compatibilidad con cache de pagina mientras mantiene personalizacion * por usuario. * * @version 1.0.0 * @see openspec/specs/adsense-javascript-first/spec.md */ (function() { 'use strict'; const VERSION = '1.0.0'; const CACHE_KEY = 'roi_adsense_visibility'; const CACHE_VERSION_KEY = 'roi_adsense_settings_version'; // Configuracion inyectada por PHP via wp_localize_script const config = window.roiAdsenseConfig || {}; /** * Logger condicional (solo en modo debug) */ function log(message, type = 'log') { if (!config.debug) return; const prefix = '[ROI AdSense v' + VERSION + ']'; console[type](prefix, message); } /** * Detecta si el dispositivo es movil basado en viewport */ function isMobile() { return window.innerWidth < 992; } /** * Obtiene decision cacheada de localStorage */ function getCachedDecision() { try { const cached = localStorage.getItem(CACHE_KEY); const cachedVersion = localStorage.getItem(CACHE_VERSION_KEY); if (!cached) { log('No hay decision en cache'); return null; } // Invalidar si la version de settings cambio if (cachedVersion !== config.settingsVersion) { log('Version de settings cambio, invalidando cache'); localStorage.removeItem(CACHE_KEY); localStorage.removeItem(CACHE_VERSION_KEY); return null; } const data = JSON.parse(cached); // Verificar expiracion const now = Math.floor(Date.now() / 1000); const expiresAt = data.timestamp + data.cache_seconds; if (now > expiresAt) { log('Cache expirado'); localStorage.removeItem(CACHE_KEY); return null; } log('Usando decision cacheada: ' + (data.show_ads ? 'MOSTRAR' : 'OCULTAR')); return data; } catch (e) { log('Error leyendo cache: ' + e.message, 'error'); return null; } } /** * Guarda decision en localStorage */ function cacheDecision(decision) { try { localStorage.setItem(CACHE_KEY, JSON.stringify(decision)); localStorage.setItem(CACHE_VERSION_KEY, config.settingsVersion); log('Decision cacheada por ' + decision.cache_seconds + 's'); } catch (e) { log('Error guardando cache: ' + e.message, 'warn'); } } /** * Consulta el endpoint REST para obtener decision de visibilidad */ async function fetchVisibilityDecision() { const url = new URL(config.endpoint); url.searchParams.append('post_id', config.postId); if (config.nonce) { url.searchParams.append('nonce', config.nonce); } log('Consultando endpoint: ' + url.toString()); const response = await fetch(url.toString(), { method: 'GET', credentials: 'same-origin', headers: { 'Accept': 'application/json' } }); if (!response.ok) { throw new Error('HTTP ' + response.status); } return await response.json(); } /** * Activa los anuncios (muestra placeholders, carga AdSense) */ function activateAds() { log('Activando anuncios'); // Remover clase de oculto de los containers document.querySelectorAll('.roi-adsense-placeholder').forEach(function(el) { el.classList.remove('roi-adsense-hidden'); el.classList.add('roi-adsense-active'); }); // Disparar evento para que otros scripts puedan reaccionar document.dispatchEvent(new CustomEvent('roiAdsenseActivated', { detail: { version: VERSION } })); } /** * Desactiva los anuncios (oculta placeholders) */ function deactivateAds(reasons) { log('Desactivando anuncios. Razones: ' + reasons.join(', ')); // Agregar clase de oculto a los containers document.querySelectorAll('.roi-adsense-placeholder').forEach(function(el) { el.classList.add('roi-adsense-hidden'); el.classList.remove('roi-adsense-active'); }); // Disparar evento document.dispatchEvent(new CustomEvent('roiAdsenseDeactivated', { detail: { reasons: reasons, version: VERSION } })); } /** * Aplica decision de visibilidad */ function applyDecision(decision) { if (decision.show_ads) { activateAds(); } else { deactivateAds(decision.reasons || []); } } /** * Maneja error segun estrategia de fallback configurada */ function handleError(error) { log('Error: ' + error.message, 'error'); const cached = getCachedDecision(); switch (config.fallbackStrategy) { case 'cached-or-show': if (cached) { log('Usando cache como fallback'); applyDecision(cached); } else { log('Sin cache, mostrando ads por defecto (proteger revenue)'); activateAds(); } break; case 'cached-or-hide': if (cached) { log('Usando cache como fallback'); applyDecision(cached); } else { log('Sin cache, ocultando ads por defecto'); deactivateAds(['fallback_no_cache']); } break; case 'always-show': log('Fallback: siempre mostrar'); activateAds(); break; default: log('Estrategia desconocida, mostrando ads'); activateAds(); } } /** * Funcion principal de inicializacion */ async function init() { log('Inicializando...'); // Verificar que el feature este habilitado if (!config.featureEnabled) { log('Feature deshabilitado, usando modo legacy', 'warn'); return; } // IMPORTANTE: postId = 0 es valido (paginas de archivo, home, etc.) // Solo validar que endpoint exista y postId no sea undefined/null if (!config.endpoint || config.postId === undefined || config.postId === null) { log('Sin endpoint configurado, activando ads', 'warn'); activateAds(); return; } // Intentar usar cache primero const cached = getCachedDecision(); if (cached) { applyDecision(cached); return; } // Consultar endpoint try { const decision = await fetchVisibilityDecision(); log('Respuesta del servidor: ' + JSON.stringify(decision)); // Cachear decision cacheDecision(decision); // Aplicar decision applyDecision(decision); } catch (error) { handleError(error); } } // Ejecutar cuando el DOM este listo if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // Exponer API publica para debugging window.roiAdsenseVisibility = { version: VERSION, getConfig: function() { return config; }, getCachedDecision: getCachedDecision, clearCache: function() { localStorage.removeItem(CACHE_KEY); localStorage.removeItem(CACHE_VERSION_KEY); log('Cache limpiado'); }, forceRefresh: async function() { this.clearCache(); await init(); } }; })();