- Add REST endpoint GET /roi-theme/v1/adsense-placement/visibility - Add Domain layer: UserContext, VisibilityDecision, AdsenseSettings VOs - Add Application layer: CheckAdsenseVisibilityUseCase - Add Infrastructure: AdsenseVisibilityChecker, Controller, Enqueuer - Add JavaScript controller with localStorage caching - Add test plan for production validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
273 lines
8.0 KiB
JavaScript
273 lines
8.0 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|
|
};
|
|
|
|
})();
|