/** * Script standalone para criação dinâmica do jogo de roleta * Pode ser usado via JWT ou função global window.sellflux_game() */ (function() { 'use strict'; // Lista de países com máscaras e códigos de discagem const countryList = [ { country_code: "AD", phone_mask: "999-999", country_name: "Andorra", regionCode: "376", emoji: "🇦🇩", }, { country_code: "AE", phone_mask: "(99) 999-9999", country_name: "Emirados Árabes Unidos", regionCode: "971", emoji: "🇦🇪", }, { country_code: "AF", phone_mask: "(99) 999-9999", country_name: "Afeganistão", regionCode: "93", emoji: "🇦🇫", }, { country_code: "AL", phone_mask: "(99) 999-9999", country_name: "Albânia", regionCode: "355", emoji: "🇦🇱", }, { country_code: "AM", phone_mask: "(99) 999-9999", country_name: "Armênia", regionCode: "374", emoji: "🇦🇲", }, { country_code: "AO", phone_mask: "(99) 999-9999", country_name: "Angola", regionCode: "244", emoji: "🇦🇴", }, { country_code: "AR", phone_mask: "(99) 9999-9999", country_name: "Argentina", regionCode: "54", emoji: "🇦🇷", }, { country_code: "AT", phone_mask: "(9) 9999-9999", country_name: "Áustria", regionCode: "43", emoji: "🇦🇹", }, { country_code: "AU", phone_mask: "(99) 9999-9999", country_name: "Austrália", regionCode: "61", emoji: "🇦🇺", }, { country_code: "AZ", phone_mask: "(99) 999-9999", country_name: "Azerbaijão", regionCode: "994", emoji: "🇦🇿", }, { country_code: "BA", phone_mask: "(99) 999-999", country_name: "Bósnia e Herzegovina", regionCode: "387", emoji: "🇧🇦", }, { country_code: "BD", phone_mask: "(99) 9999-9999", country_name: "Bangladesh", regionCode: "880", emoji: "🇧🇩", }, { country_code: "BE", phone_mask: "(99) 999-99-99", country_name: "Bélgica", regionCode: "32", emoji: "🇧🇪", }, { country_code: "BG", phone_mask: "(999) 999-999", country_name: "Bulgária", regionCode: "359", emoji: "🇧🇬", }, { country_code: "BH", phone_mask: "9999-9999", country_name: "Butão", regionCode: "975", emoji: "🇧🇹", }, { country_code: "BJ", phone_mask: "9999-99999", country_name: "Benim", regionCode: "229", emoji: "🇧🇯", }, { country_code: "BO", phone_mask: "(9) 999-9999", country_name: "Bolívia", regionCode: "591", emoji: "🇧🇴", }, { country_code: "BR", phone_mask: "(99) 99999-9999", country_name: "Brasil", regionCode: "55", selected: true, emoji: "🇧🇷", }, { country_code: "BY", phone_mask: "(99) 999-99-99", country_name: "Belarus", regionCode: "375", emoji: "🇧🇾", }, { country_code: "CA", phone_mask: "(999) 999-9999", country_name: "Canadá", regionCode: "1", emoji: "🇨🇦", }, { country_code: "CD", phone_mask: "(99) 999-9999", country_name: "República Democrática do Congo", regionCode: "243", emoji: "🇨🇩", }, { country_code: "CH", phone_mask: "(99) 999-99-99", country_name: "Suíça", regionCode: "41", emoji: "🇨🇭", }, { country_code: "CI", phone_mask: "(99) 99-99-99", country_name: "Costa do Marfim", regionCode: "225", emoji: "🇨🇮", }, { country_code: "CL", phone_mask: "(99) 9999-9999", country_name: "Chile", regionCode: "56", emoji: "🇨🇱", }, { country_code: "CM", phone_mask: "(99) 9999-9999", country_name: "Camarões", regionCode: "237", emoji: "🇨🇲", }, { country_code: "CO", phone_mask: "(9) 999-9999", country_name: "Colômbia", regionCode: "57", emoji: "🇨🇴", }, { country_code: "CV", phone_mask: "(99) 999-9999", country_name: "Cabo Verde", regionCode: "238", emoji: "🇨🇻", }, { country_code: "CY", phone_mask: "9999-9999", country_name: "Chipre", regionCode: "357", emoji: "🇨🇾", }, { country_code: "CZ", phone_mask: "999-999-999", country_name: "República Tcheca", regionCode: "420", emoji: "🇨🇿", }, { country_code: "DE", phone_mask: "(99) 9999-9999", country_name: "Alemanha", regionCode: "49", emoji: "🇩🇪", }, { country_code: "DK", phone_mask: "(99) 9999-9999", country_name: "Dinamarca", regionCode: "45", emoji: "🇩🇰", }, { country_code: "DO", phone_mask: "(999) 999-9999", country_name: "República Dominicana", regionCode: "1", emoji: "🇩🇴", }, { country_code: "EC", phone_mask: "(99) 999-9999", country_name: "Equador", regionCode: "593", emoji: "🇪🇨", }, { country_code: "EE", phone_mask: "(99) 9999-9999", country_name: "Estônia", regionCode: "372", emoji: "🇪🇪", }, { country_code: "EG", phone_mask: "(99) 9999-9999", country_name: "Egito", regionCode: "20", emoji: "🇪🇬", }, { country_code: "ES", phone_mask: "999-999-999", country_name: "Espanha", regionCode: "34", emoji: "🇪🇸", }, { country_code: "ET", phone_mask: "999-999-999", country_name: "Etiópia", regionCode: "251", emoji: "🇪🇹", }, { country_code: "FI", phone_mask: "999-999-999", country_name: "Finlândia", regionCode: "358", emoji: "🇫🇮", }, { country_code: "FO", phone_mask: "999999", country_name: "Ilhas Faroé", regionCode: "298", emoji: "🇫🇴", }, { country_code: "FR", phone_mask: "(99) 9999-9999", country_name: "França", regionCode: "33", emoji: "🇫🇷", }, { country_code: "GB", phone_mask: "(99) 9999-9999", country_name: "Reino Unido", regionCode: "44", emoji: "🇬🇧", }, { country_code: "GE", phone_mask: "(99) 9999-9999", country_name: "Geórgia", regionCode: "995", emoji: "🇬🇪", }, { country_code: "GH", phone_mask: "(99) 9999-9999", country_name: "Gana", regionCode: "233", emoji: "🇬🇭", }, { country_code: "GI", phone_mask: "99999", country_name: "Gibraltar", regionCode: "350", emoji: "🇬🇮", }, { country_code: "GL", phone_mask: "99999", country_name: "Groenlândia", regionCode: "299", emoji: "🇬🇱", }, { country_code: "GR", phone_mask: "9999-999999", country_name: "Grécia", regionCode: "30", emoji: "🇬🇷", }, { country_code: "HR", phone_mask: "(99) 999-9999", country_name: "Croácia", regionCode: "385", emoji: "🇭🇷", }, { country_code: "HU", phone_mask: "(99) 999-9999", country_name: "Hungria", regionCode: "36", emoji: "🇭🇺", }, { country_code: "ID", phone_mask: "(99) 9999-9999", country_name: "Indonésia", regionCode: "62", emoji: "🇮🇩", }, { country_code: "IE", phone_mask: "(99) 999-9999", country_name: "Irlanda", regionCode: "353", emoji: "🇮🇪", }, { country_code: "IL", phone_mask: "99-999-9999", country_name: "Israel", regionCode: "972", emoji: "🇮🇱", }, { country_code: "IM", phone_mask: "(99) 9999-9999", country_name: "Ilha de Man", regionCode: "44", emoji: "🇮🇲", }, { country_code: "IN", phone_mask: "(99) 9999-9999", country_name: "Índia", regionCode: "91", emoji: "🇮🇳", }, { country_code: "IQ", phone_mask: "(99) 9999-9999", country_name: "Iraque", regionCode: "964", emoji: "🇮🇶", }, { country_code: "IR", phone_mask: "(9999) 9999-9999", country_name: "Irã", regionCode: "98", emoji: "🇮🇷", }, { country_code: "IS", phone_mask: "999-9999", country_name: "Islândia", regionCode: "354", emoji: "🇮🇸", }, { country_code: "IT", phone_mask: "(99) 9999-9999", country_name: "Itália", regionCode: "39", emoji: "🇮🇹", }, { country_code: "JP", phone_mask: "(99) 9999-9999", country_name: "Japão", regionCode: "81", emoji: "🇯🇵", }, { country_code: "JO", phone_mask: "(99) 999-9999", country_name: "Jordânia", regionCode: "962", emoji: "🇯🇴", }, { country_code: "KE", phone_mask: "(99) 9999-999", country_name: "Quênia", regionCode: "254", emoji: "🇰🇪", }, { country_code: "KR", phone_mask: "(99) 999-9999", country_name: "Coreia do Sul", regionCode: "82", emoji: "🇰🇷", }, { country_code: "KZ", phone_mask: "(999) 999-9999", country_name: "Cazaquistão", regionCode: "7", emoji: "🇰🇿", }, { country_code: "LB", phone_mask: "(99) 999-999", country_name: "Líbano", regionCode: "961", emoji: "🇱🇧", }, { country_code: "LI", phone_mask: "999 9999", country_name: "Liechtenstein", regionCode: "423", emoji: "🇱🇮", }, { country_code: "LT", phone_mask: "(999) 99999", country_name: "Lituânia", regionCode: "370", emoji: "🇱🇹", }, { country_code: "LU", phone_mask: "999-999", country_name: "Luxemburgo", regionCode: "352", emoji: "🇱🇺", }, { country_code: "LV", phone_mask: "9999-9999", country_name: "Letônia", regionCode: "371", emoji: "🇱🇻", }, { country_code: "MA", phone_mask: "9999-99999", country_name: "Marrocos", regionCode: "212", emoji: "🇲🇦", }, { country_code: "MC", phone_mask: "99-99-99-99", country_name: "Mônaco", regionCode: "377", emoji: "🇲🇨", }, { country_code: "MD", phone_mask: "999-9999", country_name: "Moldávia", regionCode: "373", emoji: "🇲🇩", }, { country_code: "ME", phone_mask: "99-99-9999", country_name: "Montenegro", regionCode: "382", emoji: "🇲🇪", }, { country_code: "MK", phone_mask: "99-999-999", country_name: "Macedônia do Norte", regionCode: "389", emoji: "🇲🇰", }, { country_code: "MM", phone_mask: "9999-9999", country_name: "Mianmar", regionCode: "95", emoji: "🇲🇲", }, { country_code: "MT", phone_mask: "9999-9999", country_name: "Malta", regionCode: "356", emoji: "🇲🇹", }, { country_code: "MX", phone_mask: "(999) 999-9999", country_name: "México", regionCode: "52", emoji: "🇲🇽", }, { country_code: "MY", phone_mask: "9999-9999", country_name: "Malásia", regionCode: "60", emoji: "🇲🇾", }, { country_code: "MZ", phone_mask: "99-999-9999", country_name: "Moçambique", regionCode: "258", emoji: "🇲🇿", }, { country_code: "NG", phone_mask: "9999-9999", country_name: "Nigéria", regionCode: "234", emoji: "🇳🇬", }, { country_code: "NL", phone_mask: "(99) 999-9999", country_name: "Países Baixos", regionCode: "31", emoji: "🇳🇱", }, { country_code: "NO", phone_mask: "999-999-99", country_name: "Noruega", regionCode: "47", emoji: "🇳🇴", }, { country_code: "NP", phone_mask: "99-999-999", country_name: "Nepal", regionCode: "977", emoji: "🇳🇵", }, { country_code: "NZ", phone_mask: "9-9999-9999", country_name: "Nova Zelândia", regionCode: "64", emoji: "🇳🇿", }, { country_code: "PA", phone_mask: "9999-9999", country_name: "Panamá", regionCode: "507", emoji: "🇵🇦", }, { country_code: "PE", phone_mask: "999-999999", country_name: "Peru", regionCode: "51", emoji: "🇵🇪", }, { country_code: "PH", phone_mask: "(99) 9999-9999", country_name: "Filipinas", regionCode: "63", emoji: "🇵🇭", }, { country_code: "PK", phone_mask: "(999) 9999999", country_name: "Paquistão", regionCode: "92", emoji: "🇵🇰", }, { country_code: "PL", phone_mask: "999-999-999", country_name: "Polônia", regionCode: "48", emoji: "🇵🇱", }, { country_code: "PT", phone_mask: "999-999-999", country_name: "Portugal", regionCode: "351", emoji: "🇵🇹", }, { country_code: "PY", phone_mask: "(99) 999-9999", country_name: "Paraguai", regionCode: "595", emoji: "🇵🇾", }, { country_code: "RO", phone_mask: "(99) 999-9999", country_name: "Romênia", regionCode: "40", emoji: "🇷🇴", }, { country_code: "RS", phone_mask: "(99) 999-9999", country_name: "Sérvia", regionCode: "381", emoji: "🇷🇸", }, { country_code: "RU", phone_mask: "999-999-99-99", country_name: "Rússia", regionCode: "7", emoji: "🇷🇺", }, { country_code: "SA", phone_mask: "999-999-9999", country_name: "Arábia Saudita", regionCode: "966", emoji: "🇸🇦", }, { country_code: "SD", phone_mask: "99999999", country_name: "Sudão", regionCode: "249", emoji: "🇸🇩", }, { country_code: "SE", phone_mask: "99-999-99-99", country_name: "Suécia", regionCode: "46", emoji: "🇸🇪", }, { country_code: "SG", phone_mask: "9999-9999", country_name: "Singapura", regionCode: "65", emoji: "🇸🇬", }, { country_code: "SI", phone_mask: "99-999-999", country_name: "Eslovênia", regionCode: "386", emoji: "🇸🇮", }, { country_code: "SK", phone_mask: "99-9999-999", country_name: "Eslováquia", regionCode: "421", emoji: "🇸🇰", }, { country_code: "SM", phone_mask: "999-999-999", country_name: "San Marino", regionCode: "378", emoji: "🇸🇲", }, { country_code: "SO", phone_mask: "99999999", country_name: "Somália", regionCode: "252", emoji: "🇸🇴", }, { country_code: "SY", phone_mask: "9999-9999", country_name: "Síria", regionCode: "963", emoji: "🇸🇾", }, { country_code: "TH", phone_mask: "(99) 999-9999", country_name: "Tailândia", regionCode: "66", emoji: "🇹🇭", }, { country_code: "TN", phone_mask: "(99) 999-999", country_name: "Tunísia", regionCode: "216", emoji: "🇹🇳", }, { country_code: "TR", phone_mask: "(999) 999-9999", country_name: "Turquia", regionCode: "90", emoji: "🇹🇷", }, { country_code: "TZ", phone_mask: "999-999-999", country_name: "Tanzânia", regionCode: "255", emoji: "🇹🇿", }, { country_code: "UA", phone_mask: "99-999-99-99", country_name: "Ucrânia", regionCode: "380", emoji: "🇺🇦", }, { country_code: "UG", phone_mask: "999-999-999", country_name: "Uganda", regionCode: "256", emoji: "🇺🇬", }, { country_code: "US", phone_mask: "(999) 999-9999", country_name: "Estados Unidos", regionCode: "1", emoji: "🇺🇸", }, { country_code: "UY", phone_mask: "9999-9999", country_name: "Uruguai", regionCode: "598", emoji: "🇺🇾", }, { country_code: "VA", phone_mask: "(9999) 9999-9999", country_name: "Cidade do Vaticano", regionCode: "39", emoji: "🇻🇦", }, { country_code: "VE", phone_mask: "(99) 999-9999", country_name: "Venezuela", regionCode: "58", emoji: "🇻🇪", }, { country_code: "VN", phone_mask: "(99) 9999-9999", country_name: "Vietnã", regionCode: "84", emoji: "🇻🇳", }, { country_code: "XK", phone_mask: "(99) 999-999", country_name: "Kosovo", regionCode: "383", emoji: "🇽🇰", }, { country_code: "ZM", phone_mask: "(99) 999-9999", country_name: "Zâmbia", regionCode: "260", emoji: "🇿🇲", }, { country_code: "ZW", phone_mask: "(99) 999-9999", country_name: "Zimbábue", regionCode: "263", emoji: "🇿🇼", }, ]; // Função principal que inicializa o jogo function initRouletteGame(config) { // Marca que estamos na versão full para aplicar estilos específicos document.body.classList.add('sfx_sellflux-full'); // Se config não for passado, tenta obter via JWT dos parâmetros if (!config) { config = getConfigFromJWT(); if (!config) return; } // Validar configuração if (!config.settings) { console.error('Configuração inválida para o jogo - settings é obrigatório'); return; } // SUBSTITUIÇÃO: uso da nova função reutilizável renderGameContent(document.body, config); setupGameLogic(config); } // ==== RENDERIZAÇÃO DO JOGO COM SUPORTE A SHADOW DOM ==== function renderGameContent(parentElement, config, styleTarget = document.head) { // Aplica estilos e injeta o HTML principal do jogo no contêiner desejado injectStyles(config.settings, styleTarget); const gameContainer = createGameHTML(config); parentElement.appendChild(gameContainer); // Carregar possíveis dados salvos do usuário loadSavedUserData(); } // Função para gerenciar cookies do usuário function manageCookies() { return { // Salvar dados do usuário nos cookies saveUserData: function(userData) { const expires = new Date(); expires.setTime(expires.getTime() + (365 * 24 * 60 * 60 * 1000)); // 1 ano const cookieData = { name: userData.name || '', email: userData.email || '', phone: userData.phone || '', phoneWithDdi: userData.phoneWithDdi || '', countryCode: userData.countryCode || '55' }; document.cookie = `sellflux_user_data=${JSON.stringify(cookieData)}; expires=${expires.toUTCString()}; path=/; SameSite=Lax`; }, // Carregar dados do usuário dos cookies loadUserData: function() { const cookies = document.cookie.split(';'); for (let cookie of cookies) { const [name, value] = cookie.trim().split('='); if (name === 'sellflux_user_data') { try { return JSON.parse(decodeURIComponent(value)); } catch (e) { console.warn('Erro ao carregar dados do cookie:', e); return null; } } } return null; }, // Salvar timestamp da primeira participação saveFirstParticipationTimestamp: function() { const expires = new Date(); expires.setTime(expires.getTime() + (365 * 24 * 60 * 60 * 1000)); // 1 ano const timestamp = new Date().getTime(); document.cookie = `sellflux_first_participation=${timestamp}; expires=${expires.toUTCString()}; path=/; SameSite=Lax`; }, // Carregar timestamp da primeira participação loadFirstParticipationTimestamp: function() { const cookies = document.cookie.split(';'); for (let cookie of cookies) { const [name, value] = cookie.trim().split('='); if (name === 'sellflux_first_participation') { try { return parseInt(value); } catch (e) { console.warn('Erro ao carregar timestamp do cookie:', e); return null; } } } return null; }, // Verificar se pode mostrar o modal novamente baseado na frequência canShowModalAgain: function(reappearanceFrequencyHours) { const firstParticipation = this.loadFirstParticipationTimestamp(); // Se não há registro, é a primeira vez if (!firstParticipation) { return true; } // Se frequência é 0, sempre mostrar if (reappearanceFrequencyHours === 0) { return true; } // Calcular diferença em horas const now = new Date().getTime(); const diffInHours = (now - firstParticipation) / (1000 * 60 * 60); return diffInHours >= reappearanceFrequencyHours; }, // Limpar dados dos cookies clearUserData: function() { document.cookie = 'sellflux_user_data=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Lax'; }, // Limpar timestamp de participação clearParticipationTimestamp: function() { document.cookie = 'sellflux_first_participation=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Lax'; } }; } // NOVO: função utilitária para envio dos dados do lead evitando problemas de CORS // Tenta usar navigator.sendBeacon (não sofre bloqueio de CORS). Caso não haja suporte, // utiliza XMLHttpRequest com cabeçalho simples (application/json) que não exige preflight. function sendLeadRequest(url, data, onSuccess, onError) { let query=window.location.search if(query){ url=url+query } fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json;charset=UTF-8' }, body: JSON.stringify(data) }) .then(response => { if (response.ok) { if (onSuccess) onSuccess(); } else { throw new Error(`Erro HTTP: ${response.status}`); } }) .catch(error => { console.error('Erro na requisição:', error); if (onError) onError(error); }); } /** * Codifica string para HTML usando entidades numéricas para caracteres não-ASCII e escapa &, <, >, " * Garante renderização correta de acentos em páginas com encoding não-UTF-8. */ function encodeHtml(str){ if (str === undefined || str === null) return ''; let out = ''; for (let i = 0; i < str.length; i++) { const cp = str.codePointAt(i); if (cp === undefined) continue; if (cp > 0xFFFF) i++; if (cp === 34) out += '"'; else if (cp === 38) out += '&'; else if (cp === 60) out += '<'; else if (cp === 62) out += '>'; else if (cp < 32 || cp > 126) out += '&#x' + cp.toString(16).toUpperCase() + ';'; else out += String.fromCharCode(cp); } return out; } // Função para carregar dados salvos nos campos do formulário function loadSavedUserData() { const cookieManager = manageCookies(); const savedData = cookieManager.loadUserData(); if (savedData) { // Preencher os campos com dados salvos setTimeout(() => { const nameField = document.getElementById('name'); const emailField = document.getElementById('email'); const phoneField = document.getElementById('phone'); const countrySelect = document.getElementById('country-select'); if (nameField && savedData.name) nameField.value = savedData.name; if (emailField && savedData.email) emailField.value = savedData.email; // CORREÇÃO: Primeiro definir o país e depois o telefone if (countrySelect && savedData.countryCode) { countrySelect.value = savedData.countryCode; } // CORREÇÃO: Para o telefone, verificar se há um número formatado salvo (sem DDI) if (phoneField && savedData.phone) { // Se o phone salvo tem DDI (+55...), extrair apenas os números após o DDI let phoneNumber = savedData.phone; if (phoneNumber.startsWith('+')) { // Remove o DDI (ex: +5511999999999 -> 11999999999) const countryCode = savedData.countryCode || '55'; const ddiRegex = new RegExp('^\\+' + countryCode); phoneNumber = phoneNumber.replace(ddiRegex, ''); } // Garantir que é apenas números phoneNumber = phoneNumber.replace(/\D/g, ''); phoneField.value = phoneNumber; // Aplicar máscara após definir o valor if (countrySelect) { countrySelect.dispatchEvent(new Event('change')); } } // Adicionar feedback visual para campos preenchidos [nameField, emailField, phoneField].forEach(field => { if (field && field.value) { field.classList.add('sfx_field-prefilled'); } }); }, 100); } } // Função para obter configuração via JWT (método original) function getConfigFromJWT() { // Decodifica JWT sem biblioteca externa function parseJwt(token) { try { const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); return JSON.parse(jsonPayload); } catch (e) { console.error('Erro ao decodificar JWT:', e); return null; } } // Obtém parâmetros da URL function getUrlParams() { // Tentar pegar da query string primeiro let searchParams = window.location.search; // Se não houver, tentar pegar após o hash (para iframe com blob URL) if (!searchParams && window.location.hash) { const hashParts = window.location.hash.split('?'); if (hashParts.length > 1) { searchParams = '?' + hashParts[1]; } } const params = new URLSearchParams(searchParams); const token = params.get('token'); const apiUrl = params.get('apiUrl') || '/api/lead'; if (!token) { console.error('Token JWT não fornecido'); return null; } const decoded = parseJwt(token); if (!decoded) return null; return { settings: decoded.settings || {}, prizes: decoded.prizes || [], apiUrl: apiUrl }; } return getUrlParams(); } // ==== INICIALIZA JOGO DENTRO DO MODAL ISOLADO ==== function initRouletteGameModal(config, shadowRoot) { // Validar configuração if (!config.settings) { console.error('Configuração inválida para o jogo - settings é obrigatório'); return; } const modalContent = shadowRoot.getElementById('modal-content'); if (!modalContent) { console.error('Container do modal não encontrado'); return; } // SUBSTITUIÇÃO: uso da nova função reutilizável (estilos no ShadowRoot) renderGameContent(modalContent, config, shadowRoot); setupGameLogicModal(config); // A lógica permanece a mesma graças ao patch de DOM } window.sellflux_button_cta = function(settings, prizes, apiUrl) { // ================= CONFIGURAÇÃO BÁSICA ================= const config = { settings: { title: 'Gire e Ganhe!', subtitle: 'Preencha seus dados e gire a roleta para ganhar prêmios incríveis!', buttonText: 'Girar a Roleta!', buttonOpenModalText: 'SORTEIO', logoUrl: '', bannerUrl: '', backgroundColor: '#000000', titleColor: '#ffffff', buttonColor: '#f56a00', infoColor: '#ffffff', borderColor: '#000000', reappearanceFrequency: 0, openingDelay: 0, custom_keys: [], email_field: true, phone_field: true, name_field: true, showPlayAgain: true, buttonConfig: { position: 'right', // 'left' | 'right' type: 'text', // 'text' | 'image' text: 'CTA', // texto mostrado quando type = text imageUrl: '', // url da imagem quando type = image modalPosition: 'center', // 'center' | 'anchored' ...((settings && settings.buttonConfig) || {}) }, ...settings }, prizes: prizes || [], apiUrl: apiUrl || '/api/webhook/lead' }; // ================= CRIAÇÃO DO BOTÃO CTA ================= const btnCfg = config.settings.buttonConfig; // Evita duplicação const existing = document.getElementById('sellflux-cta-button'); if (existing) existing.remove(); const ctaWrapper = document.createElement('div'); ctaWrapper.id = 'sellflux-cta-button'; ctaWrapper.style.position = 'fixed'; ctaWrapper.style.bottom = '20px'; ctaWrapper.style[btnCfg.position === 'left' ? 'left' : 'right'] = '20px'; ctaWrapper.style.zIndex = '9999999'; // Configuração baseada no tipo const isMobile = window.innerWidth <= 768; if (btnCfg.type === 'text') { // Botão de texto - largura dinâmica, cantos levemente arredondados ctaWrapper.style.width = 'auto'; ctaWrapper.style.height = 'auto'; ctaWrapper.style.borderRadius = '5px'; ctaWrapper.style.background = config.settings.buttonColor; ctaWrapper.style.display = 'inline-block'; ctaWrapper.style.padding = isMobile ? '10px 15px' : '12px 20px'; // Limitar largura em mobile para não ultrapassar a tela if (isMobile) { ctaWrapper.style.maxWidth = 'calc(100vw - 60px)'; // 20px margem + 20px segurança de cada lado ctaWrapper.style.overflow = 'hidden'; } } else { // Botão de imagem - circular como WhatsApp (menor em mobile) const size = isMobile ? '48px' : '56px'; ctaWrapper.style.width = size; ctaWrapper.style.height = size; ctaWrapper.style.borderRadius = '50%'; ctaWrapper.style.background = config.settings.buttonColor; ctaWrapper.style.display = 'flex'; ctaWrapper.style.alignItems = 'center'; ctaWrapper.style.justifyContent = 'center'; } let innerElem; if (btnCfg.type === 'image' && btnCfg.imageUrl) { innerElem = new Image(); innerElem.src = btnCfg.imageUrl; innerElem.alt = btnCfg.text || 'CTA'; // Tamanho da imagem proporcional ao container const imageSize = isMobile ? '30px' : '36px'; innerElem.style.width = imageSize; innerElem.style.height = imageSize; // innerElem.style.borderRadius = '50%'; innerElem.style.objectFit = 'cover'; innerElem.style.pointerEvents = 'none'; } else { innerElem = document.createElement('span'); innerElem.textContent = btnCfg.text || config.settings.buttonOpenModalText; innerElem.style.color = config.settings.infoColor; innerElem.style.fontSize = isMobile ? '12px' : '14px'; innerElem.style.fontWeight = 'bold'; innerElem.style.whiteSpace = isMobile ? 'normal' : 'nowrap'; innerElem.style.pointerEvents = 'none'; innerElem.style.textAlign = 'center'; innerElem.style.lineHeight = isMobile ? '1.2' : '1'; // Permitir quebra de linha em mobile se necessário if (isMobile) { innerElem.style.wordBreak = 'break-word'; innerElem.style.hyphens = 'auto'; } } // Sombra sutil ctaWrapper.style.boxShadow = '0 3px 10px rgba(0,0,0,0.25)'; ctaWrapper.style.transition = 'transform 0.3s ease'; ctaWrapper.addEventListener('mouseenter', () => { ctaWrapper.style.transform = 'scale(1.05)'; }); ctaWrapper.addEventListener('mouseleave', () => { ctaWrapper.style.transform = 'scale(1)'; }); // Cursor pointer para ambos os tipos ctaWrapper.style.cursor = 'pointer'; ctaWrapper.appendChild(innerElem); document.body.appendChild(ctaWrapper); // ================= CLICK HANDLER ================= ctaWrapper.addEventListener('click', function() { // Evitar abrir múltiplos modais if (document.getElementById('sellflux-modal-overlay')) return; // Usa a função interna existente para criar e mostrar o modal const isAnchored = btnCfg.modalPosition === 'anchored'; const shadow = showModalDirectly(config, isAnchored); // Ajusta posicionamento se solicitado if (isAnchored && shadow) { const container = shadow.getElementById('sellflux-modal-container'); if (container) { // Posiciona o modal acima do botão CTA const ctaRect = ctaWrapper.getBoundingClientRect(); const bottomOffset = window.innerHeight - ctaRect.top + 8; // 8px de margem const modalIsMobile = window.innerWidth <= 768; container.style.position = 'fixed'; container.style.bottom = bottomOffset + 'px'; // Ajustes para mobile if (modalIsMobile) { container.style.maxWidth = 'calc(100vw - 20px)'; container.style.minWidth = '280px'; container.style.left = '10px'; container.style.right = '10px'; container.style.width = 'auto'; } else { if (btnCfg.position === 'left') { container.style.left = (ctaRect.left) + 'px'; } else { container.style.right = (window.innerWidth - ctaRect.right) + 'px'; } } // Adiciona seta estilo chat (ajustada para mobile) const arrow = document.createElement('div'); arrow.style.position = 'absolute'; arrow.style.width = '0'; arrow.style.height = '0'; arrow.style.border = (modalIsMobile ? '8px' : '10px') + ' solid transparent'; arrow.style.borderTopColor = config.settings.backgroundColor || '#ffffff'; arrow.style.bottom = modalIsMobile ? '-16px' : '-20px'; if (modalIsMobile) { // Em mobile, posiciona a seta baseada na posição do botão const arrowPosition = btnCfg.position === 'left' ? '30px' : 'calc(100% - 30px)'; arrow.style.left = arrowPosition; } else { arrow.style.left = btnCfg.position === 'left' ? '25px' : 'auto'; arrow.style.right = btnCfg.position === 'right' ? '25px' : 'auto'; } container.appendChild(arrow); } } }); }; // Função global para inicializar o jogo (nova abordagem) window.sellflux_game = function(settings, prizes, apiUrl) { // Limpar body antes de criar novo jogo document.body.innerHTML = ''; const config = { settings: { title: 'Gire e Ganhe!', subtitle: 'Preencha seus dados e gire a roleta para ganhar prêmios incríveis!', buttonText: 'Girar a Roleta!', buttonOpenModalText: 'SORTEIO', logoUrl: '', bannerUrl: '', backgroundColor: '#000000', titleColor: '#ffffff', buttonColor: '#f56a00', infoColor: '#ffffff', borderColor: '#000000', reappearanceFrequency: 0, // em horas sendo zero sempre que voltar openingDelay: 0, // em segundos sendo zero imediatamente, custom_keys: [], email_field: true, phone_field: true, name_field: true, showPlayAgain: true, // VALOR PADRÃO ...settings // MESCLA configurações fornecidas preservando padrões }, prizes: prizes || [], apiUrl: apiUrl || '/api/webhook/lead' }; initRouletteGame(config); }; // Função global para modal com controle de frequência de reaparecimento window.sellflux_game_modal = function(settings, prizes, apiUrl) { const config = { settings: { title: 'Gire e Ganhe!', subtitle: 'Preencha seus dados e gire a roleta para ganhar prêmios incríveis!', buttonText: 'Girar a Roleta!', buttonOpenModalText: 'SORTEIO', logoUrl: '', bannerUrl: '', backgroundColor: '#000000', titleColor: '#ffffff', buttonColor: '#f56a00', infoColor: '#ffffff', borderColor: '#000000', reappearanceFrequency: 0, // em horas sendo zero sempre que voltar openingDelay: 0, // em segundos sendo zero imediatamente, custom_keys: [], email_field: true, phone_field: true, name_field: true, showPlayAgain: true, // VALOR PADRÃO ...settings // MESCLA configurações fornecidas preservando padrões }, prizes: prizes || [], apiUrl: apiUrl || '/api/webhook/lead' }; // Sempre criar o botão flutuante primeiro createFloatingButton(config); const cookieManager = manageCookies(); const reappearanceFrequencyHours = config.settings.reappearanceFrequency || 0; // Verificar se deve mostrar o modal automaticamente baseado na frequência de reaparecimento if (cookieManager.canShowModalAgain(reappearanceFrequencyHours)) { // Pode mostrar o modal automaticamente - aplicar delay se configurado const openingDelaySeconds = config.settings.openingDelay || 0; if (openingDelaySeconds > 0) { setTimeout(() => { showModalDirectly(config); }, openingDelaySeconds * 1000); } else { showModalDirectly(config); } } // Se não pode mostrar automaticamente, o botão flutuante já foi criado acima }; // ==== EXIBE O MODAL ISOLADO ==== function showModalDirectly(config, isAnchoredMode = false) { // Evita recriar/atualizar o modal caso ele já esteja aberto // Isso impede que o timer de abertura automática "derrube" a edição em andamento if (document.getElementById('sellflux-modal-overlay')) { return; // Modal já visível. Não faz nada. } const shadowRoot = createModalOverlay(config, isAnchoredMode); initRouletteGameModal(config, shadowRoot); return shadowRoot; // Retorna shadowRoot para ajustes externos } // Função para criar o botão flutuante function createFloatingButton(config) { // Remover botão existente se houver const existingButton = document.getElementById('sellflux-floating-button'); if (existingButton) { existingButton.remove(); } const floatingButton = document.createElement('div'); floatingButton.id = 'sellflux-floating-button'; if(config.settings.buttonOpenModalText){ floatingButton.innerHTML = ` `; } // Adicionar estilos para o botão flutuante const buttonStyles = document.createElement('style'); buttonStyles.id = 'sellflux-floating-button-styles'; // Remover estilos existentes se houver const existingStyles = document.getElementById('sellflux-floating-button-styles'); if (existingStyles) { existingStyles.remove(); } buttonStyles.textContent = ` #sellflux-floating-button { position: fixed; bottom: 20px; left: 20px; z-index: 999999; animation: fadeIn 1s ease-in-out; } .sfx_floating-button-content { background: ${config.settings.backgroundColor || '#000000'}; color: ${config.settings.infoColor || '#ffffff'}; border: none; border-radius: 50px; padding: 15px 25px; font-size: 16px; font-weight: bold; cursor: pointer; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); transition: all 0.3s ease; text-transform: uppercase; letter-spacing: 1px; } .sfx_floating-button-content:hover { box-shadow: 0 6px 25px rgba(0, 0, 0, 0.4); opacity: 0.9; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @media (max-width: 768px) { #sellflux-floating-button { bottom: 15px; left: 15px; } .sfx_floating-button-content { padding: 12px 20px; font-size: 14px; } } `; document.head.appendChild(buttonStyles); document.body.appendChild(floatingButton); // Adicionar evento de clique para abrir o modal (sem remover o botão) floatingButton.addEventListener('click', () => { showModalDirectly(config); }); } // ==== CRIAÇÃO DO MODAL COM ISOLAMENTO VIA SHADOW DOM ==== function createModalOverlay(config, isAnchoredMode = false) { // Remover overlay existente se houver const existingOverlay = document.getElementById('sellflux-modal-overlay'); if (existingOverlay) { existingOverlay.remove(); } // Wrapper que ficará no DOM principal apenas para posicionamento const overlay = document.createElement('div'); overlay.id = 'sellflux-modal-overlay'; overlay.style.position = 'fixed'; // Configuração diferenciada para modo ancorado (chat) if (isAnchoredMode) { overlay.style.width = '0'; overlay.style.height = '0'; overlay.style.overflow = 'visible'; } else { overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; } overlay.style.zIndex = '1000000'; // Cria Shadow Root para isolar estilos const shadow = overlay.attachShadow({ mode: 'open' }); // Estrutura interna do modal const overlayStyles = isAnchoredMode ? ` .sfx_modal-overlay { position: relative; width: auto; height: auto; background-color: transparent; z-index: 1000000; display: block; pointer-events: none; }` : ` .sfx_modal-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; /* ocupa viewport, sem gerar scroll extra */ background-color: rgba(0, 0, 0, 0.8); z-index: 1000000; display: flex; justify-content: center; align-items: center; padding: 40px ; /* cria respiro top/bottom */ box-sizing: border-box; animation: fadeInModal 0.3s ease-out; }`; shadow.innerHTML = `
`; // Patch temporário para que as chamadas existentes continuem funcionando (function patchDomForShadow(dom, sr){ const origGetById = dom.getElementById.bind(dom); dom.getElementById = function(id){ return sr.getElementById(id) || origGetById(id); }; const origQuery = dom.querySelector.bind(dom); dom.querySelector = function(sel){ return sr.querySelector(sel) || origQuery(sel); }; })(document, shadow); document.body.appendChild(overlay); // Função para fechar modal (sem remover botão flutuante) function closeModal() { overlay.remove(); } // Adicionar evento para fechar o modal const closeBtn = shadow.getElementById('modal-close-btn'); closeBtn.addEventListener('click', closeModal); // Fechar modal ao clicar no overlay (fora do container) shadow.querySelector('.sfx_modal-overlay').addEventListener('click', (e) => { if (e.target.classList && e.target.classList.contains('sfx_modal-overlay')) { closeModal(); } }); // Fechar modal com ESC function escHandler(e) { if (e.key === 'Escape') { closeModal(); document.removeEventListener('keydown', escHandler); } } document.addEventListener('keydown', escHandler); // Retornar shadowRoot para uso posterior return shadow; } // ==== INJEÇÃO DE ESTILOS ==== // Permite escolher onde o