migrado de js para go
This commit is contained in:
parent
663a8d5bf2
commit
6f78511946
7 changed files with 964 additions and 83 deletions
|
|
@ -1,4 +1,14 @@
|
|||
(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,
|
||||
|
|
@ -8,51 +18,55 @@
|
|||
data_minima_abertura: '',
|
||||
};
|
||||
|
||||
function parseDataMinima(s){
|
||||
// Aceita somente ISO (data) YYYY-MM-DD.
|
||||
// Retorna um Date no início do dia (00:00) no horário local.
|
||||
const v = String(s || '').trim();
|
||||
if(!v) return null;
|
||||
const m = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(v);
|
||||
if(!m) return null;
|
||||
const ano = Number(m[1]);
|
||||
const mes = Number(m[2]);
|
||||
const dia = Number(m[3]);
|
||||
if(!ano || mes < 1 || mes > 12 || dia < 1 || dia > 31) return null;
|
||||
return new Date(ano, mes-1, dia, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
function antesDaDataMinima(cfg){
|
||||
const d = parseDataMinima(cfg.data_minima_abertura);
|
||||
if(!d) return false;
|
||||
return new Date() < d;
|
||||
}
|
||||
|
||||
function normalizeEmail(email){
|
||||
return String(email || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function cooldownKey(produto, inquilino, usuarioCodigo){
|
||||
// Prefixo de storage atualizado para o novo nome do projeto.
|
||||
return `eli-nps:cooldown:${produto}:${inquilino}:${usuarioCodigo}`;
|
||||
}
|
||||
|
||||
function nowMs(){ return Date.now(); }
|
||||
// ------------------------------------------------------------------
|
||||
// WASM (Go)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
function withinCooldown(key){
|
||||
try{
|
||||
const v = localStorage.getItem(key);
|
||||
if(!v) return false;
|
||||
const obj = JSON.parse(v);
|
||||
return obj && obj.until && nowMs() < obj.until;
|
||||
}catch(e){ return false; }
|
||||
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 setCooldown(key, hours){
|
||||
try{
|
||||
const until = nowMs() + hours*3600*1000;
|
||||
localStorage.setItem(key, JSON.stringify({until}));
|
||||
}catch(e){}
|
||||
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(){
|
||||
|
|
@ -133,71 +147,43 @@
|
|||
init: async function(opts){
|
||||
const cfg = Object.assign({}, DEFAULTS, opts || {});
|
||||
|
||||
// Bloqueio por data mínima (feature flag simples).
|
||||
// Ex.: não abrir modal antes de 2026-01-01.
|
||||
if(antesDaDataMinima(cfg)){
|
||||
return;
|
||||
}
|
||||
// Carrega WASM (Go). Sem WASM, não abrimos o widget (fail-closed).
|
||||
const okWasm = await carregarWasm(cfg.apiBase);
|
||||
if(!okWasm) return;
|
||||
|
||||
// produto_nome pode ser qualquer string (ex.: "e-licencie", "Cachaça & Churras").
|
||||
// Regra do projeto: o tratamento/normalização de caracteres deve ser feito
|
||||
// apenas no backend, exclusivamente para nome de tabela/rotas.
|
||||
const produtoNome = String(cfg.produto_nome || '').trim();
|
||||
const inquilino = String(cfg.inquilino_codigo || '').trim();
|
||||
const usuarioCodigo = String(cfg.usuario_codigo || '').trim();
|
||||
const email = normalizeEmail(cfg.usuario_email);
|
||||
// Pré-validação e preparação do payload no WASM.
|
||||
const pre = window.__eli_nps_wasm_preflight(cfg);
|
||||
if(!pre || !pre.ok) return;
|
||||
|
||||
// controle de exibição: produto + inquilino_codigo + usuario_codigo
|
||||
if(!produtoNome || !inquilino || !usuarioCodigo){
|
||||
return; // missing required context
|
||||
}
|
||||
|
||||
// A chave do cooldown é “best-effort” e não participa de nenhuma regra
|
||||
// de segurança. Mantemos o produto como foi informado.
|
||||
const chaveCooldown = cooldownKey(produtoNome, inquilino, usuarioCodigo);
|
||||
if(withinCooldown(chaveCooldown)) return;
|
||||
|
||||
// Enviamos exatamente o produto_nome informado.
|
||||
const payload = {
|
||||
produto_nome: produtoNome,
|
||||
inquilino_codigo: inquilino,
|
||||
inquilino_nome: String(cfg.inquilino_nome || '').trim(),
|
||||
usuario_codigo: usuarioCodigo,
|
||||
usuario_nome: String(cfg.usuario_nome || '').trim(),
|
||||
usuario_telefone: String(cfg.usuario_telefone || '').trim(),
|
||||
usuario_email: email,
|
||||
};
|
||||
// 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`, payload);
|
||||
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
|
||||
}
|
||||
|
||||
if(!data || !data.pode_abrir || !data.id){
|
||||
// small cooldown to avoid flicker if backend keeps rejecting
|
||||
setCooldown(chaveCooldown, cfg.cooldownHours);
|
||||
return;
|
||||
}
|
||||
|
||||
// Backend can return normalized product; use it for building iframe URL.
|
||||
const produtoRota = data.produto;
|
||||
if(!produtoRota){
|
||||
// fail-closed (não dá pra montar URL segura)
|
||||
setCooldown(chaveCooldown, cfg.cooldownHours);
|
||||
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/${produtoRota}/${data.id}/form`;
|
||||
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).
|
||||
setCooldown(chaveCooldown, cfg.cooldownHours);
|
||||
window.__eli_nps_wasm_set_cooldown(pre.chave_cooldown, dec.cooldown_ate_ms);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue