1/** 2 * Widget de captação OneSendz. 3 * 4 * O cliente cola UMA linha no site dele e ganha o botão flutuante de WhatsApp 5 * com formulário: a pessoa preenche nome, e-mail e telefone, os dados caem na 6 * conta dele como contato (com a tag da fonte, o que dispara as automações) e 7 * só então o WhatsApp abre, já com a mensagem escrita. 8 * 9 * Por que existe: sem ele, quem clica no botão de WhatsApp de um site chega ao 10 * inbox como um número desconhecido e uma conversa vazia. O e-mail, que é o que 11 * permite continuar falando com quem não comprou, nunca é capturado. 12 * 13 * Decisões que importam: 14 * 15 * - Shadow DOM. O widget roda dentro do site do cliente, com CSS que a gente 16 * não controla. Sem o shadow, um `button { width: 100% }` do tema dele 17 * deforma tudo. Também impede que os nossos estilos vazem para o site dele. 18 * 19 * - `keepalive` no envio. O clique navega para o WhatsApp quase junto; sem 20 * keepalive o navegador cancela a requisição no meio e o lead se perde 21 * justamente nos que convertem. Com ele, o envio termina mesmo saindo da 22 * página, e por isso não esperamos resposta para abrir a conversa. 23 * 24 * - `application/x-www-form-urlencoded`. Evita o preflight do CORS: uma 25 * requisição a menos para dar errado no site de terceiro. 26 * 27 * - Nada de dependência externa nem de rede além do próprio envio. 28 * 29 * Uso: 30 * <script src="https://app.onesendz.com/w.js" 31 * data-token="SEU_TOKEN" 32 * data-phone="5511999999999" 33 * data-logo="https://seusite.com.br/logo.png" 34 * data-branding="off"></script> 35 * 36 * Dentro da página, em vez do botão flutuante: 37 * <div id="form-onesendz"></div> 38 * <script src="https://app.onesendz.com/w.js" 39 * data-token="SEU_TOKEN" 40 * data-phone="5511999999999" 41 * data-target="#form-onesendz"></script> 42 */ 43(function () { 44 'use strict'; 45 46 var atual = document.currentScript; 47 if (!atual) return; // sem o script não há configuração 48 if (window.__onesendzWidget) return; // dois embeds na mesma página 49 window.__onesendzWidget = true; 50 51 var local = atual.dataset || {}; 52 var token = (local.token || '').trim(); 53 if (!token) { 54 console.warn('[OneSendz] widget precisa de data-token'); 55 return; 56 } 57 // O telefone NÃO é exigido aqui de propósito: ele pode vir do painel, e é o 58 // que permite trocar o número sem mexer no site. Quem não tem nos dois 59 // lugares é avisado lá embaixo, quando já sabemos o que o painel mandou. 60 61 var base = (local.endpoint || 'https://app.onesendz.com/api/webhooks/lead').trim(); 62 63 /** 64 * Aparência vem do painel; o que está no código é o que vale se ela não vier. 65 * 66 * Essa ordem é o que permite colar o código uma vez só e depois mudar cor e 67 * textos pelo painel, sem ninguém tocar no site de novo. E é o que mantém o 68 * widget de pé quando a resposta não chega: os valores do código já bastam. 69 * 70 * Buscamos ANTES de desenhar, em vez de desenhar e corrigir depois, porque 71 * corrigir depois piscaria na cara de quem está olhando. A espera é curta e 72 * quase sempre nem existe, já que a resposta fica guardada na borda e no 73 * navegador; passando de 1,5 s desistimos e desenhamos com o que temos. 74 */ 75 function comConfig(pronto) { 76 var respondeu = false; 77 var prazo = setTimeout(function () { 78 if (respondeu) return; 79 respondeu = true; 80 pronto({}); 81 }, 1500); 82 83 try { 84 fetch(base + (base.indexOf('?') >= 0 ? '&' : '?') + 't=' + encodeURIComponent(token) + '&config=1', { 85 method: 'GET', 86 mode: 'cors', 87 credentials: 'omit', 88 }) 89 .then(function (r) { return r.ok ? r.json() : {}; }) 90 // Dois callbacks em vez de .then().catch(): assim uma falha DENTRO do 91 // desenho do widget não cai no tratador de erro de rede, que a 92 // engoliria em silêncio e deixaria a página sem botão nenhum, sem 93 // ninguém conseguir descobrir por quê. 94 .then( 95 function (j) { 96 if (respondeu) return; 97 respondeu = true; 98 clearTimeout(prazo); 99 pronto(j && typeof j === 'object' ? j : {}); 100 }, 101 function () { 102 if (respondeu) return; 103 respondeu = true; 104 clearTimeout(prazo); 105 pronto({}); 106 }, 107 ); 108 } catch (_) { 109 if (!respondeu) { respondeu = true; clearTimeout(prazo); pronto({}); } 110 } 111 } 112 113 comConfig(function (servidor) { 114 // O painel tem a última palavra; vazio ou ausente cai no valor do código. 115 function opcao(nomePainel, nomeLocal) { 116 var v = servidor[nomePainel]; 117 if (v === undefined || v === null || String(v).trim() === '') return local[nomeLocal] || ''; 118 return String(v); 119 } 120 121 var cfg = { 122 phone: opcao('phone', 'phone'), 123 // RodÃzio Inteligente (/wa/<slug>): cada lead cai no número que ainda tem franquia de atendimento. 124 zap: opcao('zap', 'zap'), 125 color: opcao('color', 'color'), 126 title: opcao('title', 'title'),
127 subtitle: opcao('subtitle', 'subtitle'), 128 cta: opcao('cta', 'cta'), 129 text: opcao('text', 'text'), 130 logo: opcao('logo', 'logo'), 131 position: opcao('position', 'position'), 132 fields: opcao('fields', 'fields'), 133 country: opcao('country', 'country'), 134 // Onde o formulário vive: flutuando no canto ou dentro de um lugar da 135 // página. Fica no código e não no painel porque depende de existir um 136 // elemento no site para receber, e isso o painel não tem como saber. 137 target: (local.target || '').trim(), 138 // branding é liga/desliga: só o false explÃcito do painel desliga. 139 branding: servidor.branding === false ? 'off' : (local.branding || ''), 140 emailOptional: servidor.email_optional === true ? 'true' : (local.emailOptional || ''), 141 // Idioma dos textos: data-lang no código, senão o idioma da org (o 142 // servidor só manda quando não é português), senão português. 143 lang: (local.lang || servidor.locale || 'pt-BR'), 144 }; 145 montar(cfg); 146 }); 147 148 // ââ Textos por idioma âââââââââââââââââââââââââââââââââââââââââââââââââââââ 149 // Português é o de sempre; espanhol neutro da América Latina; inglês. 150 var TEXTOS = { 151 pt: { 152 titulo: 'Olá! Tudo bem?', 153 subtitulo: 'Preencha os campos abaixo para iniciar a conversa no WhatsApp.', 154 cta: 'Iniciar conversa', 155 saudacao: 'Oi! Vim pelo site e quero saber mais.', 156 nome: 'Nome', email: 'E-mail', emailOpc: 'E-mail (opcional)', pais: 'PaÃs', outro: 'Outro paÃs', 157 foneBr: 'DDD + número *', fone: 'Número *', foneOutro: 'Número com código do paÃs *', 158 erroBr: 'Confira o número: DDD + 8 ou 9 dÃgitos.', erroPais: 'Confira o número para ', 159 erroOutro: 'Digite o número com o código do paÃs.', abrindo: 'Abrindo...', 160 meuNome: ' Meu nome é ', feito: 'Feito com ', abrir: 'Falar no WhatsApp', fechar: 'Fechar', 161 paises: { BR: 'Brasil', PT: 'Portugal', US: 'Estados Unidos', AR: 'Argentina', UY: 'Uruguai', PY: 'Paraguai', 162 CL: 'Chile', ES: 'Espanha', MX: 'México', CO: 'Colômbia', PE: 'Peru', EC: 'Equador', BO: 'BolÃvia', VE: 'Venezuela' }, 163 }, 164 es: { 165 titulo: '¡Hola! ¿Qué tal?', 166 subtitulo: 'Completa los campos para iniciar la conversación por WhatsApp.', 167 cta: 'Iniciar conversación', 168 saudacao: '¡Hola! Vengo del sitio web y quiero saber más.', 169 nome: 'Nombre', email: 'Correo', emailOpc: 'Correo (opcional)', pais: 'PaÃs', outro: 'Otro paÃs', 170 foneBr: 'DDD + número *', fone: 'Número *', foneOutro: 'Número con código de paÃs *', 171 erroBr: 'Revisa el número: DDD + 8 o 9 dÃgitos.', erroPais: 'Revisa el número para ', 172 erroOutro: 'Escribe el número con el código de paÃs.', abrindo: 'Abriendo...', 173 meuNome: ' Mi nombre es ', feito: 'Hecho con ', abrir: 'Hablar por WhatsApp', fechar: 'Cerrar', 174 paises: { BR: 'Brasil', PT: 'Portugal', US: 'Estados Unidos', AR: 'Argentina', UY: 'Uruguay', PY: 'Paraguay', 175 CL: 'Chile', ES: 'España', MX: 'México', CO: 'Colombia', PE: 'Perú', EC: 'Ecuador', BO: 'Bolivia', VE: 'Venezuela' }, 176 }, 177 en: { 178 titulo: 'Hi there!', 179 subtitulo: 'Fill in the fields below to start the chat on WhatsApp.', 180 cta: 'Start chat', 181 saudacao: 'Hi! I came from the website and want to know more.', 182 nome: 'Name', email: 'Email', emailOpc: 'Email (optional)', pais: 'Country', outro: 'Other country', 183 foneBr: 'Area code + number *', fone: 'Number *', foneOutro: 'Number with country code *', 184 erroBr: 'Check the number: area code + 8 or 9 digits.', erroPais: 'Check the number for ', 185 erroOutro: 'Type the number with the country code.', abrindo: 'Opening...', 186 meuNome: ' My name is ', feito: 'Made with ', abrir: 'Chat on WhatsApp', fechar: 'Close', 187 paises: { BR: 'Brazil', PT: 'Portugal', US: 'United States', AR: 'Argentina', UY: 'Uruguay', PY: 'Paraguay', 188 CL: 'Chile', ES: 'Spain', MX: 'Mexico', CO: 'Colombia', PE: 'Peru', EC: 'Ecuador', BO: 'Bolivia', VE: 'Venezuela' }, 189 }, 190 }; 191 192 function montar(cfg) { 193 var idioma = String(cfg.lang || '').toLowerCase(); 194 var T = idioma.indexOf('es') === 0 ? TEXTOS.es : idioma.indexOf('en') === 0 ? TEXTOS.en : TEXTOS.pt; 195 var fone = (cfg.phone || '').replace(/\D/g, ''); 196 var zap = String(cfg.zap || '').toLowerCase().replace(/[^a-z0-9-]/g, ''); 197 if (!fone && !zap) { 198 console.warn('[OneSendz] sem número de WhatsApp: configure no painel, em Contatos > Captar leads, ou passe data-phone no script'); 199 return; 200 } 201 var cor = (cfg.color || '#128C7E').trim(); 202 var titulo = cfg.title || T.titulo; 203 var subtitulo = cfg.subtitle || T.subtitulo; 204 var cta = cfg.cta || T.cta; 205 var saudacao = cfg.text || T.saudacao; 206 var esquerda = (cfg.position || 'right').toLowerCase() === 'left'; 207 // Logo da marca acima dos campos. Sem ela o formulário fica correto porém 208 // anônimo, e quem chega não reconhece de quem é a janela que abriu. 209 var logo = (cfg.logo || '').trim(); 210 // Assinatura discreta no rodapé. Sai com data-branding="off": o site é do 211 // cliente, e marca de terceiro que não dá para tirar vira motivo de briga. 212 var marca = String(cfg.branding || '').toLowerCase() !== 'off'; 213 // Quais campos pedir. Menos campos = mais gente preenche; o telefone fica 214 // sempre, porque é o que liga o lead à conversa que vem depois. 215 var campos = (cfg.fields || 'nome,email,telefone').toLowerCase().split(',').map(function (c) { 216 return c.trim(); 217 }); 218 var pedeNome = campos.indexOf('nome') >= 0; 219 var pedeEmail = campos.indexOf('email') >= 0; 220 var emailOpcional = String(cfg.emailOptional || '') === 'true'; 221
222 // ââ PaÃses ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 223 // 224 // Lista curta de propósito. Um seletor com 200 paÃses vira um problema por 225 // si só: a pessoa rola procurando o dela em vez de preencher o formulário. 226 // Estes cobrem quase todo o público de um negócio brasileiro, e quem estiver 227 // fora escolhe "Outro paÃs" e digita o número completo. 228 // 229 // Formato: [ISO, código, nome, dÃgitos mÃnimos, dÃgitos máximos]. 230 // O nome sai no idioma do formulário. 231 var PAISES = [ 232 ['BR', '55', 'Brasil', 10, 11], 233 ['PT', '351', 'Portugal', 9, 9], 234 ['US', '1', 'Estados Unidos', 10, 10], 235 ['AR', '54', 'Argentina', 10, 11], 236 ['UY', '598', 'Uruguai', 8, 9], 237 ['PY', '595', 'Paraguai', 9, 9], 238 ['CL', '56', 'Chile', 9, 9], 239 ['ES', '34', 'Espanha', 9, 9], 240 ['MX', '52', 'México', 10, 10], 241 ['CO', '57', 'Colômbia', 10, 10], 242 ['PE', '51', 'Peru', 9, 9], 243 ['EC', '593', 'Equador', 9, 9], 244 ['BO', '591', 'BolÃvia', 8, 8], 245 ['VE', '58', 'Venezuela', 10, 10], 246 ].map(function (pa) { return [pa[0], pa[1], T.paises[pa[0]] || pa[2], pa[3], pa[4]]; }); 247 248 249 // Sem bandeiras aqui, e é decisão, não esquecimento: o emoji de bandeira não 250 // existe no Windows (o navegador mostra "BR" em letras), e um <select> nativo 251 // não aceita imagem dentro das opções. Para ter bandeira de verdade seria 252 // preciso trocar o seletor nativo por um menu próprio, que é justamente o que 253 // funciona pior no celular. "BR +55" é legÃvel em todo lugar e igual para 254 // todo mundo. 255 256 /** 257 * Qual paÃs já vem escolhido. 258 * 259 * Ordem: o que o cliente fixou, depois a região do navegador da pessoa 260 * (pt-BR, es-AR), e por último o paÃs do número do próprio negócio, que é o 261 * melhor palpite quando o navegador não diz a região. Nada disso usa rede. 262 */ 263 function paisInicial() { 264 var forcado = String(cfg.country || '').toUpperCase(); 265 if (forcado && acharIso(forcado)) return forcado; 266 267 // O paÃs do NEGÃCIO vem primeiro, e não o idioma do navegador de quem 268 // visita. Quem atende de um +55 recebe quase só gente do Brasil, e muita 269 // gente navega com o navegador em inglês ou espanhol sem morar lá: pelo 270 // idioma, um uruguaio com o navegador em espanhol da Espanha abria o 271 // formulário marcado como Espanha. 272 var porCodigo = null; 273 for (var j = 0; j < PAISES.length; j++) { 274 if (fone.indexOf(PAISES[j][1]) === 0) { 275 if (!porCodigo || PAISES[j][1].length > porCodigo[1].length) porCodigo = PAISES[j]; 276 } 277 } 278 if (porCodigo) return porCodigo[0]; 279 280 // Só quando o número do negócio não diz nada é que o navegador decide. 281 var idiomas = (navigator.languages && navigator.languages.length) 282 ? navigator.languages : [navigator.language || '']; 283 for (var i = 0; i < idiomas.length; i++) { 284 var m = String(idiomas[i]).match(/[-_]([A-Za-z]{2})$/); 285 if (m && acharIso(m[1].toUpperCase())) return m[1].toUpperCase(); 286 } 287 return 'BR'; 288 } 289 290 function acharIso(iso) { 291 for (var i = 0; i < PAISES.length; i++) if (PAISES[i][0] === iso) return PAISES[i]; 292 return null; 293 } 294 295 // ââ Marcação ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 296 var host = document.createElement('div'); 297 host.setAttribute('data-onesendz', 'widget'); 298 var raiz = host.attachShadow ? host.attachShadow({ mode: 'open' }) : host; 299 300 var css = document.createElement('style'); 301 css.textContent = [ 302 ':host,*{box-sizing:border-box}', 303 '.wrap{position:fixed;bottom:20px;' + (esquerda ? 'left' : 'right') + ':20px;z-index:2147483000;', 304 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}', 305 '.btn{width:60px;height:60px;border-radius:50%;border:0;cursor:pointer;background:#25D366;', 306 'box-shadow:0 4px 14px rgba(0,0,0,.28);display:flex;align-items:center;justify-content:center;', 307 'padding:0;transition:transform .15s ease}', 308 '.btn:hover{transform:scale(1.06)}', 309 '.btn:focus-visible{outline:3px solid #0b5;outline-offset:3px}', 310 '.btn svg{width:34px;height:34px;fill:#fff}', 311 '.painel{position:absolute;bottom:74px;' + (esquerda ? 'left' : 'right') + ':0;width:320px;max-width:calc(100vw - 40px);', 312 'background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 10px 40px rgba(0,0,0,.25);', 313 'opacity:0;transform:translateY(8px);pointer-events:none;transition:opacity .16s ease,transform .16s ease}', 314 '.painel.on{opacity:1;transform:none;pointer-events:auto}', 315 '.top{background:' + cor + ';color:#fff;padding:14px 40px 14px 16px;position:relative}', 316 '.top h3{margin:0 0 4px;font-size:15px;font-weight:600;line-height:1.3}', 317 '.top p{margin:0;font-size:12.5px;line-height:1.4;
317opacity:.92}', 318 '.x{position:absolute;top:10px;right:10px;width:26px;height:26px;border:0;border-radius:50%;', 319 'background:transparent;color:#fff;font-size:19px;line-height:1;cursor:pointer;padding:0}', 320 '.x:hover{background:rgba(255,255,255,.18)}', 321 '.logo{display:flex;justify-content:center;padding:14px 16px 2px;background:#fff}', 322 '.logo img{max-height:64px;max-width:150px;width:auto;height:auto;object-fit:contain}', 323 '.form{padding:14px 16px 16px;display:flex;flex-direction:column;gap:9px;background:#fff}', 324 '.form input{width:100%;padding:10px 12px;border:1px solid #d7dbdd;border-radius:7px;', 325 'font-size:14px;color:#111;background:#fff;font-family:inherit}', 326 '.form input:focus{outline:0;border-color:' + cor + ';box-shadow:0 0 0 3px rgba(18,140,126,.15)}', 327 '.form input::placeholder{color:#97a2a6}', 328 '.fone{display:flex;gap:6px}', 329 '.fone select{flex:0 0 auto;width:104px;padding:10px 6px;border:1px solid #d7dbdd;border-radius:7px;', 330 'font-size:14px;color:#111;background:#fff;font-family:inherit;cursor:pointer}', 331 '.fone select:focus{outline:0;border-color:' + cor + ';box-shadow:0 0 0 3px rgba(18,140,126,.15)}', 332 '.fone input{flex:1 1 auto;min-width:0}', 333 '.enviar{margin-top:3px;padding:11px;border:0;border-radius:7px;background:' + cor + ';color:#fff;', 334 'font-size:14.5px;font-weight:600;cursor:pointer;font-family:inherit}', 335 '.enviar:hover{filter:brightness(1.08)}', 336 '.enviar[disabled]{opacity:.6;cursor:default}', 337 '.erro{margin:0;font-size:12px;color:#c0392b;min-height:0}', 338 '.marca{margin:2px 0 0;text-align:center;font-size:10px;color:#9aa5a8;line-height:1.3}', 339 '.marca a{color:#8b989c;text-decoration:none;border-bottom:1px solid transparent}', 340 '.marca a:hover{color:' + cor + ';border-bottom-color:currentColor}', 341 '@media (max-width:420px){.painel{width:calc(100vw - 32px)}.wrap{bottom:16px;' + (esquerda ? 'left' : 'right') + ':16px}}', 342 // Embutido: sem canto fixo, sem botão, e o formulário sempre aberto, 343 // ocupando a largura que o site der. 344 '.wrap.dentro{position:static;width:100%;max-width:420px;margin:0 auto}', 345 '.wrap.dentro .painel{position:static;width:100%;max-width:none;opacity:1;transform:none;pointer-events:auto}', 346 '.wrap.dentro .btn,.wrap.dentro .x{display:none}', 347 '@media (prefers-reduced-motion:reduce){.btn,.painel{transition:none}}', 348 ].join(''); 349 350 var wrap = document.createElement('div'); 351 wrap.className = 'wrap'; 352 353 var painel = document.createElement('div'); 354 painel.className = 'painel'; 355 painel.setAttribute('role', 'dialog'); 356 painel.setAttribute('aria-label', titulo); 357 358 var top = document.createElement('div'); 359 top.className = 'top'; 360 var h3 = document.createElement('h3'); 361 h3.textContent = titulo; // textContent, nunca innerHTML 362 var ps = document.createElement('p'); 363 ps.textContent = subtitulo; 364 var fechar = document.createElement('button'); 365 fechar.className = 'x'; 366 fechar.type = 'button'; 367 fechar.setAttribute('aria-label', T.fechar); 368 fechar.textContent = 'Ã'; 369 top.appendChild(h3); top.appendChild(ps); top.appendChild(fechar); 370 371 var form = document.createElement('form'); 372 form.className = 'form'; 373 374 function campo(nome, tipo, placeholder, obrigatorio) { 375 var i = document.createElement('input'); 376 i.name = nome; i.type = tipo; 377 // Asterisco no obrigatório, como as pessoas já esperam num formulário. 378 i.placeholder = obrigatorio ? placeholder + ' *' : placeholder; 379 i.autocomplete = nome === 'email' ? 'email' : nome === 'telefone' ? 'tel' : 'name'; 380 if (obrigatorio) i.required = true; 381 form.appendChild(i); 382 return i; 383 } 384 var iNome = pedeNome ? campo('nome', 'text', T.nome, true) : null; 385 var iEmail = pedeEmail ? campo('email', 'email', emailOpcional ? T.emailOpc : T.email, !emailOpcional) : null; 386 // Telefone com seletor de paÃs: é o que tira a ambiguidade de vez. Com o 387 // código escolhido na lista, não existe mais número de 11 dÃgitos que possa 388 // ser tanto brasileiro quanto de fora. 389 var linhaFone = document.createElement('div'); 390 linhaFone.className = 'fone'; 391 392 var selPais = document.createElement('select'); 393 selPais.setAttribute('aria-label', T.pais); 394 PAISES.forEach(function (pa) { 395 var o = document.createElement('option'); 396 o.value = pa[0]; 397 o.textContent = pa[0] + ' +' + pa[1]; 398 o.title = pa[2]; 399 selPais.appendChild(o); 400 }); 401 var outro = document.createElement('option'); 402 outro.value = '__'; 403 outro.textContent = T.outro; 404 selPais.appendChild(outro); 405 selPais.value = paisInicial(); 406 407 var iFone = document.createElement('input'); 408 iFone.name = 'telefone'; iFone.type = 'tel'; iFone.required = true; 409 iFone.autocomplete = 'tel'; 410 411 function ajustarFone() { 412 var pa = acharIso(selPais.value); 413 // "Outro paÃs": a pessoa digita tudo, com o código junto. 414 iFone.placeholder = pa 415 ? (pa[0] === 'BR' ? T.foneBr : T.fone) 416 : T.foneOutro; 417 } 418 selPais.addEventListener('change', function () { ajustarFone(); iFone.focus(); }); 419 ajustarFone(); 420 421 linhaFone.appendChild(selPais); 422 linhaFone.appendChild(iFone); 423 form.appendChild(linhaFone); 424 425 var erro = document.createElement('p'); 426 erro.className = 'erro'; 427 var enviar = document.createElement('button'); 428 enviar.className = 'enviar'; 429 enviar.type = 'submit'; 430 enviar.textContent = cta; 431 form.appendChild(erro); form.appendChild(enviar); 432 433 if (marca) { 434 var rodape = document.createElement('p'); 435 rodape.className = 'marca'; 436 var a = document.createElement('a'); 437 a.href = 'https://onesendz.com/?utm_source=widget&utm_medium=referral' 438 + '&utm_campaign=lead_widget&utm_content=' + encodeURIComponent(location.hostname); 439 a.target = '_blank'; 440 a.rel = 'noopener noreferrer'; 441 a.textContent = 'OneSendz'; 442 rodape.appendChild(document.createTextNode(T.feito)); 443 rodape.appendChild(a); 444 form.appendChild(rodape); 445 } 446 447 painel.appendChild(top); 448 if (logo) { 449 var box = document.createElement('div'); 450 box.className = 'logo'; 451 var img = document.createElement('img'); 452 img.src = logo; 453 img.alt = ''; 454 // Logo quebrada não pode deixar um buraco no formulário. 455 img.onerror = function () { box.remove(); }; 456 box.appendChild(img); 457 painel.appendChild(box); 458 } 459 painel.appendChild(form); 460 461 var botao = document.createElement('button'); 462 botao.className = 'btn'; 463 botao.type = 'button'; 464 botao.setAttribute('aria-label', T.abrir); 465 // Logo do WhatsApp (viewBox 24). Desenhado inteiro em vez de carregado de 466 // fora: o widget não pode depender de mais nenhuma requisição. 467 botao.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.
467198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893A11.821 11.821 0 0020.885 3.4"/></svg>'; 468 469 wrap.appendChild(painel); 470 // No modo embutido não há botão para abrir: o formulário já está na página. 471 if (!cfg.target) wrap.appendChild(botao); 472 raiz.appendChild(css); raiz.appendChild(wrap); 473 474 // ââ Comportamento âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 475 function abrir(v) { 476 painel.classList.toggle('on', v); 477 botao.setAttribute('aria-expanded', v ? 'true' : 'false'); 478 if (v) setTimeout(function () { (iNome || iFone).focus(); }, 60); 479 } 480 botao.addEventListener('click', function () { abrir(!painel.classList.contains('on')); }); 481 fechar.addEventListener('click', function () { abrir(false); botao.focus(); }); 482 document.addEventListener('keydown', function (e) { 483 if (e.key === 'Escape' && painel.classList.contains('on')) { abrir(false); botao.focus(); } 484 }); 485 486 form.addEventListener('submit', function (e) { 487 e.preventDefault(); 488 erro.textContent = ''; 489 490 // O número sai daqui já em formato internacional, com o código do paÃs 491 // escolhido na lista. à isso que impede o contato do formulário e o da 492 // conversa de WhatsApp virarem duas pessoas diferentes. 493 var bruto = (iFone.value || '').trim(); 494 var digitos = bruto.replace(/\D/g, ''); 495 var pais = acharIso(selPais.value); 496 var tel; 497 498 if (pais) { 499 // Tira o código do paÃs se a pessoa digitou junto, para não duplicar. 500 if (digitos.indexOf(pais[1]) === 0 && digitos.length > pais[4]) { 501 digitos = digitos.slice(pais[1].length); 502 } 503 // Tira o zero de discagem nacional (o "0" de 098... no Uruguai, 011... 504 // na Argentina). Ele existe para ligar dentro do paÃs e não entra no 505 // formato internacional. Só tiramos quando sobra um número do tamanho 506 // certo, para não mutilar um número que por acaso comece com zero. 507 if (digitos.charAt(0) === '0' && digitos.length - 1 >= pais[3]) { 508 digitos = digitos.replace(/^0+/, ''); 509 } 510 if (digitos.length < pais[3] || digitos.length > pais[4]) { 511 erro.textContent = pais[0] === 'BR' 512 ? T.erroBr 513 : T.erroPais + pais[2] + '.'; 514 iFone.focus(); 515 return; 516 } 517 // Argentina: o celular no WhatsApp é +54 9 + área + número. 518 if (pais[0] === 'AR' && digitos.length === 10) digitos = '9' + digitos; 519 tel = '+' + pais[1] + digitos; 520 } else { 521 // "Outro paÃs": aceitamos o que vier, só exigindo tamanho de número real. 522 digitos = digitos.replace(/^00/, ''); 523 if (digitos.length < 8 || digitos.length > 15) { 524 erro.textContent = T.erroOutro; 525 iFone.focus(); 526 return; 527 } 528 tel = '+' + digitos; 529 } 530 531 var dados = new URLSearchParams(); 532 if (iNome) dados.set('nome', iNome.value.trim()); 533 if (iEmail && iEmail.value.trim()) dados.set('email', iEmail.value.trim()); 534 dados.set('telefone', tel); 535 dados.set('origem', location.hostname); 536 dados.set('pagina', location.pathname.slice(0, 200)); 537 538 enviar.disabled = true; 539 enviar.textContent = T.abrindo; 540 541 // keepalive: o envio termina mesmo com a página indo para o WhatsApp. 542 // Por isso não esperamos a resposta: esperar só atrasaria a conversa, e 543 // uma falha nossa não pode impedir a pessoa de falar com a empresa. 544 try { 545 fetch(base + (base.indexOf('?') >= 0 ? '&' : '?') + 't=' + encodeURIComponent(token), { 546 method: 'POST', 547 headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, 548 body: dados.toString(), 549 keepalive: true, 550 mode: 'cors', 551 }).catch(function () { /* o WhatsApp abre de qualquer jeito */ }); 552 } catch (_) { /* idem */ } 553 554 // Avisa os rastreadores que Jà EXISTEM na página. Não carregamos pixel 555 // nenhum e não mandamos nada para lugar nenhum: se o site tem Meta ou 556 // Google, o lead entra como conversão na conta DELE; se não tem, nada 557 // acontece. Sem isto o anúncio nunca aprende com quem converteu, que é 558 // metade do valor de captar o lead. 559 try { if (typeof window.fbq === 'function') window.fbq('track', 'Lead'); } catch (_) {} 560 try { if (typeof window.gtag === 'function') window.gtag('event', 'generate_lead'); } catch (_) {} 561 // Evento próprio, para quem quiser ouvir no site sem depender de nós. 562 try { 563 window.dispatchEvent(new CustomEvent('onesendz:lead', { detail: { token: token } })); 564 } catch (_) {} 565 566 var texto = saudacao; 567 if (iNome && iNome.value.trim()) texto = saudacao + T.meuNome + iNome.value.trim() + '.'; 568 // Com link inteligente, o servidor escolhe o número (e o fixo vira reserva se o link estiver pausado). 569 var link = zap 570 ? 'https://app.onesendz.com/wa/' + zap + '?text=' + encodeURIComponent(texto) + (fone ? '&f=' + fone : '') 571 : 'https://wa.me/' + fone + '?text=' + encodeURIComponent(texto); 572 573 // Aba nova quando dá; se o navegador bloquear, navega na mesma. 574 var nova = window.open(link, '_blank', 'noopener'); 575 if (!nova) location.href = link; 576 577 setTimeout(function () { 578 enviar.disabled = false;
579 enviar.textContent = cta; 580 form.reset(); 581 abrir(false); 582 }, 900); 583 }); 584 585 function inserir() { 586 if (cfg.target) { 587 var alvo = null; 588 try { alvo = document.querySelector(cfg.target); } catch (_) { alvo = null; } 589 if (alvo) { 590 wrap.className = 'wrap dentro'; 591 painel.classList.add('on'); 592 alvo.appendChild(host); 593 return; 594 } 595 // Alvo não existe (nome errado, ou o script correu antes do elemento). 596 // Melhor virar botão flutuante do que sumir: o site continua captando e 597 // fica visÃvel que algo está fora do lugar. 598 console.warn('[OneSendz] elemento ' + cfg.target + ' não encontrado; usando o botão flutuante'); 599 } 600 (document.body || document.documentElement).appendChild(host); 601 } 602 if (document.body) inserir(); 603 else document.addEventListener('DOMContentLoaded', inserir); 604 } 605})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.