(function(){ 'use strict'; var BASE = location.origin; function getCookie(n){try{var m=document.cookie.match(new RegExp('(?:^|; )'+n+'=([^;]*)'));return m?decodeURIComponent(m[1]):'';}catch(e){return '';}} function setCookie(n,v,d){try{var e=new Date(Date.now()+d*864e5).toUTCString();document.cookie=n+'='+encodeURIComponent(v)+';expires='+e+';path=/;SameSite=Lax';}catch(e){}} function getParam(n){return new URLSearchParams(location.search).get(n)||'';} function uuid(){return (crypto&&crypto.randomUUID?crypto.randomUUID():('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,function(c){var r=Math.random()*16|0,v=c==='x'?r:(r&0x3|0x8);return v.toString(16);})));} function lsGet(k){try{return localStorage.getItem(k)||'';}catch(e){return '';}} function lsSet(k,v){try{localStorage.setItem(k,v);}catch(e){}} var sid = lsGet('crocs_sid') || getCookie('crocs_sid'); if(!sid){ sid = uuid(); lsSet('crocs_sid', sid); setCookie('crocs_sid', sid, 30); } window.__crocs_session_id = sid; var stored = {}; try{ stored = JSON.parse(lsGet('crocs_utms')||'{}'); }catch(e){} var ctx = { session_id: sid, page_url: location.href, utm_source: getParam('utm_source') || stored.utm_source || '', utm_medium: getParam('utm_medium') || stored.utm_medium || '', utm_campaign: getParam('utm_campaign') || stored.utm_campaign || '', utm_content: getParam('utm_content') || stored.utm_content || '', utm_term: getParam('utm_term') || stored.utm_term || '', ttclid: getParam('ttclid') || stored.ttclid || getCookie('_ttclid') || '', fbc: getCookie('_fbc') || stored.fbc || '', fbp: getCookie('_fbp') || stored.fbp || '', ttp: getCookie('_ttp') || getCookie('ttp') || stored.ttp || '' }; var clientData = {}; try { clientData = JSON.parse(lsGet('crocs_cd')||'{}'); } catch(e){ clientData = {}; } if (!clientData.email) clientData.email = ''; if (!clientData.phone) clientData.phone = ''; try{ lsSet('crocs_utms', JSON.stringify(ctx)); }catch(e){} // ── track interno (Live View + Funil) ─────────────────────────────────── function trackInternal(event, extra){ var payload = Object.assign({}, ctx, extra||{}, {event: event, page_url: location.href}); try { var blob = new Blob([JSON.stringify(payload)], {type:'application/json'}); if (navigator.sendBeacon && event === 'heartbeat') { navigator.sendBeacon(BASE + '/api/tracking.php', blob); return; } fetch(BASE + '/api/tracking.php', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload),keepalive:true}).catch(function(){}); } catch(e){} } window.__crocs_track = trackInternal; // ── disparar eventos TikTok server-side (via duttyfy.php) ───────────────── function fireTTEvent(ttEvent, eventId, value){ // Refresca os dados persistentes do localStorage em cada chamada // (user pode ter preenchido o form depois do boot original) try { clientData = JSON.parse(lsGet('crocs_cd')||'{}'); } catch(e){} // Re-identifica o pixel client-side para Advanced Matching sempre fresco tiktokIdentify(); var body = { session_id: sid, event: ttEvent, event_id: eventId || (ttEvent + '_' + sid + '_' + Date.now()), page_url: location.href, email: clientData.email || '', phone: clientData.phone || '', // external_id cascade: email → sid (sempre presente) // TikTok aceita qualquer string única estável → sid garante matching external_id: clientData.email || sid, first_name: clientData.first_name || '', last_name: clientData.last_name || '', ttclid: ctx.ttclid || '', ttp: ctx.ttp || '', fbc: ctx.fbc || '', fbp: ctx.fbp || '' }; if (typeof value === 'number') body.value = value; try { fetch(BASE + '/api/duttyfy.php?action=fire-tt-event', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body), keepalive: true }).catch(function(){}); } catch(e){} } window.__crocs_fire_tt = fireTTEvent; // ── ttq.identify (Advanced Matching) ─────────────────────────────────── // external_id: email se existir, senão sid. Isto garante matching sempre. function tiktokIdentify(){ if (typeof window.ttq === 'undefined' || !window.ttq.identify) return; var id = { external_id: clientData.email || sid }; if (clientData.email) id.email = clientData.email; if (clientData.phone) id.phone_number = clientData.phone; try { window.ttq.identify(id); } catch(e){} } // ── envia enriquecimento para server (para eventos server-side enriquecerem) function sendEnrich(){ var body = Object.assign({session_id: sid}, clientData, { ttclid: ctx.ttclid, ttp: ctx.ttp, fbc: ctx.fbc, fbp: ctx.fbp }); try { fetch(BASE + '/api/enrich.php', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body), keepalive: true }).catch(function(){}); } catch(e){} } window.__crocs_enrich = sendEnrich; // ── captura email/telefone em qualquer form (funciona em SPA React) ───── var _capturedEls = new WeakSet(); function bindFormCapture(){ var emailSelectors = 'input[type=email], input[name*=email], input[id*=email]'; var phoneSelectors = 'input[type=tel], input[name*=phone], input[id*=phone], input[name*=telemovel], input[name*=telefone]'; var postalSel = 'input[name*=postal], input[name*=cep], input[id*=postal], input[id*=cep]'; var citySel = 'input[name*=city], input[name*=cidade], input[id*=city], input[id*=cidade]'; var nameSel = 'input[name*=name], input[name*=nome], input[id*=name], input[id*=nome]'; function once(el, fn){ if (_capturedEls.has(el)) return; _capturedEls.add(el); el.addEventListener('blur', fn); // Também dispara no change (alguns React onChange podem não disparar blur) el.addEventListener('change', fn); } document.querySelectorAll(emailSelectors).forEach(function(el){ once(el, function(){ var v = (el.value||'').trim().toLowerCase(); // Validação mais estrita: precisa ter formato válido com TLD (.com, .pt, etc.) // Evita sobrescrever crocs_cd com emails parciais como "joao@gmail" // que o TikTok reporta como "Email address not valid". var emailRegex = /^[a-z0-9._+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i; if (v && emailRegex.test(v)) { clientData.email = v; lsSet('crocs_cd', JSON.stringify(clientData)); tiktokIdentify(); sendEnrich(); } }); }); document.querySelectorAll(phoneSelectors).forEach(function(el){ once(el, function(){ var v = (el.value||'').trim(); if (v && v.length >= 7) { // Normaliza para E.164 com "+" (exigência do TikTok/Facebook) // BR: 10-11 dígitos (DDD + 8 ou 9 dígitos) → adiciona 55 // PT: 9 dígitos começando com 9 → adiciona 351 var hasPlus = v.charAt(0) === '+'; var digits = v.replace(/[^0-9]/g,''); if (!hasPlus) { if (digits.length === 10 || digits.length === 11) { digits = '55' + digits; } else if (digits.length === 9 && digits[0] === '9') { digits = '351' + digits; } } if (digits.length >= 8 && digits.length <= 15) { clientData.phone = '+' + digits; lsSet('crocs_cd', JSON.stringify(clientData)); tiktokIdentify(); sendEnrich(); } } }); }); document.querySelectorAll(postalSel).forEach(function(el){ once(el, function(){ var v = (el.value||'').trim(); if (v) { clientData.zip = v; lsSet('crocs_cd', JSON.stringify(clientData)); sendEnrich(); } }); }); document.querySelectorAll(citySel).forEach(function(el){ once(el, function(){ var v = (el.value||'').trim(); if (v) { clientData.city = v; lsSet('crocs_cd', JSON.stringify(clientData)); sendEnrich(); } }); }); document.querySelectorAll(nameSel).forEach(function(el){ once(el, function(){ var v = (el.value||'').trim(); if (v) { var parts = v.split(/\s+/); clientData.first_name = parts[0] || ''; clientData.last_name = parts.length > 1 ? parts[parts.length-1] : ''; lsSet('crocs_cd', JSON.stringify(clientData)); sendEnrich(); } }); }); } // ── MutationObserver: re-corre bindFormCapture quando o SPA React // adiciona inputs novos ao DOM (crítico — sem isto o React não é apanhado) function observeDomForForms(){ if (!window.MutationObserver) return; var debounce = null; var obs = new MutationObserver(function(){ clearTimeout(debounce); debounce = setTimeout(bindFormCapture, 120); }); try { obs.observe(document.body || document.documentElement, { childList: true, subtree: true }); } catch(e){} } // ── A/B variant ───────────────────────────────────────────────────────── fetch(BASE + '/api/ab.php?session_id='+encodeURIComponent(sid)) .then(function(r){return r.json();}) .then(function(d){ if (d && d.ok && d.variant) { window.__crocs_variant = d.variant; try { document.querySelectorAll('[data-crocs-old-price]').forEach(function(e){ e.textContent = d.variant.old_price; }); document.querySelectorAll('[data-crocs-new-price]').forEach(function(e){ e.textContent = d.variant.new_price; }); } catch(e){} } }).catch(function(){}); function boot(){ bindFormCapture(); observeDomForForms(); // NOVO: SPA React support tiktokIdentify(); // 1) identify ANTES de qualquer evento sendEnrich(); // 2) envia IP+UA+ttclid+ttp para server trackInternal('pageview'); // 3) Live View + Funil interno setInterval(function(){ trackInternal('heartbeat'); }, 25000); window.addEventListener('pagehide', function(){ trackInternal('heartbeat'); }); // ── Fluxo de eventos TikTok (alinhado com o funil do cliente) ─────────── // • PageView → landing (carregamento da página inicial) // • ViewContent → clique em "PARTICIPAR AGORA" (saída da landing) // • InitiateCheckout → load de /pix.html (chegada ao checkout) // • AddPaymentInfo → clique em "EFETUAR PAGAMENTO" (submete form) // • Purchase → server-side quando polling confirma pagamento var path = (location.pathname || '/').toLowerCase(); // Bucket 60s — MESMO FORMATO que pixel.js.php e stableEventId (server). // Garante dedup mesmo com offsets pequenos de carregamento entre scripts. var bucket = function(){ return Math.floor(Date.now() / 1000 / 60); }; // ═════════════ LANDING ("/" ou index.html) ═════════════ if (path === '/' || path === '' || path.indexOf('index.html') >= 0) { // 1) PageView imediato — mesmo event_id que o pixel.js.php fireTTEvent('PageView', 'pv_' + sid + '_' + bucket()); // 2) ViewContent quando o user clica no CTA "PARTICIPAR AGORA" var vcFired = false; function attemptViewContent(){ if (vcFired) return; vcFired = true; var b = bucket(); fireTTEvent('ViewContent', 'vc_' + sid + '_' + b); fireTTEvent('LandingPageView', 'lpv_' + sid + '_' + b); fireTTEvent('EngagedSession', 'es_' + sid + '_' + b); } // Lista de keywords positivos (CTAs de conversão / avanço no funil) var POSITIVE_KWS = [ 'PARTICIPAR','CONTINUAR','COMEÇAR','COMECAR','RESGATAR','RESGATE', 'SAQUE','SAQUEI','QUERO RECEBER','QUERO PARTICIPAR','QUERO GANHAR', 'AVANÇAR','AVANCAR','PRÓXIMO','PROXIMO','START','OBTER','GANHAR' ]; // Lista de keywords negativos (saídas/recusas — não devem disparar VC) var NEGATIVE_KWS = ['NÃO QUERO','NAO QUERO','CANCELAR','VOLTAR','SAIR','FECHAR','DESISTIR']; document.addEventListener('click', function(e){ if (vcFired) return; var el = e.target; for (var i = 0; i < 5 && el && el !== document.body; i++) { if (!el.tagName) { el = el.parentElement; continue; } var tag = el.tagName.toLowerCase(); var txt = (el.textContent || '').toUpperCase(); var isBtn = (tag === 'button' || tag === 'a' || (el.getAttribute && (el.getAttribute('role') === 'button' || el.getAttribute('type') === 'submit'))); if (isBtn) { // Checa negativos primeiro — evita falso positivo "NÃO QUERO PARTICIPAR" var isNeg = false; for (var n = 0; n < NEGATIVE_KWS.length; n++) { if (txt.indexOf(NEGATIVE_KWS[n]) >= 0) { isNeg = true; break; } } if (isNeg) { el = el.parentElement; continue; } // Checa positivos for (var p = 0; p < POSITIVE_KWS.length; p++) { if (txt.indexOf(POSITIVE_KWS[p]) >= 0) { attemptViewContent(); return; } } } el = el.parentElement; } }, true); return; } // ═════════════ /pix.html → InitiateCheckout ═════════════ if (path.indexOf('/pix') === 0 || path.indexOf('pix.html') >= 0) { // PageView da página pix (complementa o da landing) fireTTEvent('PageView', 'pv_' + sid + '_' + bucket()); // InitiateCheckout: NÃO disparar aqui — deixar para pix.html // (que tem acesso ao _cfg.amount correto via get-config). // pix.html irá chamar __crocs_fire_tt('InitiateCheckout', icId, amount) // com o MESMO event_id persistido em wmb_ic + value válido. // Isto resolve 2 bugs: // 1. "Event ID mismatch" — client+server usam MESMO icId agora // 2. "Purchase value not valid" — amount > 0 garantido // // Apenas geramos o icId e gravamos em wmb_ic para pix.html reutilizar. if (!sessionStorage.getItem('wmb_ic')) { var icId = 'ic_' + sid + '_' + bucket(); try { sessionStorage.setItem('wmb_ic', icId); } catch(e){} } // AddPaymentInfo quando o user clica em "EFETUAR PAGAMENTO" / "GERAR PIX" // Usa sessionStorage.wmb_api para evitar duplo disparo (pix.html também dispara). var apiFired = !!sessionStorage.getItem('wmb_api'); function handleApiClick(txt){ if (apiFired || sessionStorage.getItem('wmb_api')) { apiFired = true; return; } apiFired = true; // pix.html dispara este evento (linha ~691) com amount correto. // Apenas setamos o apiId em sessionStorage para pix.html reusar. var apiId = 'api_' + sid + '_' + bucket(); try { sessionStorage.setItem('wmb_api', apiId); } catch(e){} // NÃO disparamos aqui — pix.html dispara client+server com value válido } document.addEventListener('click', function(e){ if (apiFired) return; var el = e.target; for (var i = 0; i < 5 && el && el !== document.body; i++) { if (!el.tagName) { el = el.parentElement; continue; } var tag = el.tagName.toLowerCase(); var txt = (el.textContent || '').toUpperCase(); var isBtn = (tag === 'button' || tag === 'a' || (el.getAttribute && (el.getAttribute('role') === 'button' || el.getAttribute('type') === 'submit'))); if (isBtn) { if (txt.indexOf('EFETUAR PAGAMENTO') >= 0 || txt.indexOf('EFECTUAR PAGAMENTO') >= 0 || txt.indexOf('GERAR PIX') >= 0 || txt.indexOf('GERAR QR') >= 0 || txt.indexOf('PAGAR') >= 0 || txt.indexOf('CONFIRMAR PAGAMENTO')>= 0 || txt.indexOf('FINALIZAR') >= 0) { handleApiClick(txt); return; } } el = el.parentElement; } }, true); // Backup: form submit — só seta apiId em sessionStorage, NÃO dispara // (pix.html dispara com value correto após o submit completo). document.addEventListener('submit', function(e){ if (apiFired || sessionStorage.getItem('wmb_api')) { apiFired = true; return; } apiFired = true; var apiId = 'api_' + sid + '_' + bucket(); try { sessionStorage.setItem('wmb_api', apiId); } catch(e){} }, true); return; } // ═════════════ Outras páginas (/obrigado.html, etc.) ═════════════ fireTTEvent('PageView', 'pv_' + sid + '_' + bucket()); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();