PageSourceSearch

https://app.onesendz.com/w.js

js onesendz.com collected 2026-10-04 14:10:05 UTC 29,809 bytes, 605 lines download raw bytes

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.