(function(){ // Widget NPS (arquivo único). // // Regras do projeto (.agent): // - sem dependências externas // - fail-closed // - contratos públicos estáveis // // Evolução: regras de negócio do cliente foram movidas para WASM (Go) // sempre que possível. O backend continua sendo a autoridade. const DEFAULTS = { apiBase: '', cooldownHours: 24, // Data mínima para permitir abertura do modal. // Formato ISO (data): YYYY-MM-DD (ex.: "2026-01-01"). // Se vazio, não aplica bloqueio por data. data_minima_abertura: '', }; function cooldownKey(produto, inquilino, usuarioCodigo){ // Prefixo de storage atualizado para o novo nome do projeto. return `eli-nps:cooldown:${produto}:${inquilino}:${usuarioCodigo}`; } // ------------------------------------------------------------------ // WASM (Go) // ------------------------------------------------------------------ async function carregarWasm(apiBase){ // fail-closed: se o WASM não carregar, o widget não abre. if(window.__eli_nps_wasm_ready) return true; if(window.__eli_nps_wasm_loading) return window.__eli_nps_wasm_loading; window.__eli_nps_wasm_loading = (async function(){ try{ // wasm_exec.js expõe global `Go`. if(!window.Go){ await carregarScript(`${apiBase}/static/wasm_exec.js`); } const go = new Go(); const res = await fetch(`${apiBase}/static/e-li.nps.wasm`, {cache: 'no-cache'}); if(!res.ok) return false; const bytes = await res.arrayBuffer(); const {instance} = await WebAssembly.instantiate(bytes, go.importObject); go.run(instance); return !!window.__eli_nps_wasm_ready; }catch(e){ return false; } })(); return window.__eli_nps_wasm_loading; } function carregarScript(src){ return new Promise(function(resolve, reject){ try{ const s = document.createElement('script'); s.src = src; s.async = true; s.onload = function(){ resolve(); }; s.onerror = function(){ reject(new Error('script_fail')); }; document.head.appendChild(s); }catch(e){ reject(e); } }); } function createModal(){ const host = document.createElement('div'); host.id = 'eli-nps-host'; const shadow = host.attachShadow ? host.attachShadow({mode:'open'}) : host; const style = document.createElement('style'); style.textContent = ` .eli-nps-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;z-index:2147483647;} /* Responsividade do modal do widget: - Em telas pequenas, usa quase a tela toda. - Em telas maiores, mantém tamanho máximo confortável. */ .eli-nps-panel{width:min(560px, calc(100vw - 24px));height:min(540px, calc(100vh - 24px));background:#fff;border-radius:14px;overflow:hidden;box-shadow:0 12px 44px rgba(0,0,0,.35);display:flex;flex-direction:column;} .eli-nps-header{flex:0 0 auto;display:flex;justify-content:flex-end;align-items:center;padding:10px;border-bottom:1px solid #eee;} .eli-nps-close{border:1px solid #ddd;background:#fff;border-radius:10px;padding:8px 12px;cursor:pointer;font:600 13px system-ui;} iframe{width:100%;flex:1 1 auto;border:0;} @media (max-width: 480px){ .eli-nps-panel{width:calc(100vw - 16px);height:calc(100vh - 16px);border-radius:12px;} .eli-nps-header{padding:8px;} .eli-nps-close{padding:10px 12px;font-size:14px;} } `; const backdrop = document.createElement('div'); backdrop.className = 'eli-nps-backdrop'; const panel = document.createElement('div'); panel.className = 'eli-nps-panel'; const header = document.createElement('div'); header.className = 'eli-nps-header'; const close = document.createElement('button'); close.className = 'eli-nps-close'; close.textContent = 'Fechar'; header.appendChild(close); panel.appendChild(header); backdrop.appendChild(panel); shadow.appendChild(style); shadow.appendChild(backdrop); function destroy(){ try{ host.remove(); }catch(e){} window.removeEventListener('message', onMsg); } function onMsg(ev){ if(ev && ev.data && ev.data.type === 'eli-nps:done'){ destroy(); } } close.addEventListener('click', destroy); // Importante: não fechamos o modal ao clicar fora (backdrop). // Em mobile é comum tocar fora sem querer e perder o formulário. window.addEventListener('message', onMsg); document.body.appendChild(host); return {shadow, panel, destroy}; } async function postJSON(url, body){ const res = await fetch(url, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body), }); return res; } // API pública do widget. // Nome novo do projeto: e-li.nps window.ELiNPS = { init: async function(opts){ const cfg = Object.assign({}, DEFAULTS, opts || {}); // Carrega WASM (Go). Sem WASM, não abrimos o widget (fail-closed). const okWasm = await carregarWasm(cfg.apiBase); if(!okWasm) return; // Pré-validação e preparação do payload no WASM. const pre = window.__eli_nps_wasm_preflight(cfg); if(!pre || !pre.ok) return; // Cooldown visual no browser (WASM faz storage best-effort). // A chave do cooldown é best-effort e não participa de regra de segurança. if(window.__eli_nps_wasm_cooldown_ativo(pre.chave_cooldown)) return; let data; try{ const res = await postJSON(`${cfg.apiBase}/api/e-li.nps/pedido`, pre.payload); if(!res.ok) return; // fail-closed data = await res.json(); }catch(e){ return; // fail-closed } const dec = window.__eli_nps_wasm_decidir(cfg, data); if(!dec || !dec.abrir){ // cooldown para evitar flicker se o backend seguir rejeitando. if(dec && dec.aplicar_cooldown){ window.__eli_nps_wasm_set_cooldown(pre.chave_cooldown, dec.cooldown_ate_ms); } return; } const modal = createModal(); const iframe = document.createElement('iframe'); iframe.src = `${cfg.apiBase}/e-li.nps/${dec.produto_rota}/${dec.id}/form`; modal.panel.appendChild(iframe); // Visual cooldown so it doesn't keep popping (even if user closes). window.__eli_nps_wasm_set_cooldown(pre.chave_cooldown, dec.cooldown_ate_ms); } }; })();