class PixQRGenerator { constructor() { this.initializePixInterface(); } initializePixInterface() { // Listeners for realtime preview const fieldsToWatch = [ 'pix-key', 'pix-key-type', 'pix-name', 'pix-city', 'pix-amount', 'pix-description', 'pix-txid' ]; fieldsToWatch.forEach(fieldId => { const element = document.getElementById(fieldId); if (element) { element.addEventListener('input', () => this.updatePreview()); element.addEventListener('change', () => this.updatePreview()); } }); // Add currency mask to amount field const amountInput = document.getElementById('pix-amount'); if (amountInput) { amountInput.addEventListener('input', (e) => { let value = e.target.value.replace(/\D/g, ''); if (value) { value = (parseInt(value) / 100).toFixed(2) + ''; value = value.replace('.', ','); value = value.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1.'); e.target.value = value; } this.updatePreview(); }); } } updatePreview() { const data = this.collectPixData(); const previewElement = document.getElementById('pix-preview-text'); if (previewElement) { // Validação de "Preguiça" (Laziness Validation) // Checks if user selected one type but entered another const validationWarning = this.validateKeyType(data.keyType, data.key); if (validationWarning) { previewElement.innerHTML = ` ${validationWarning}`; return; } if (!data.key || !data.name || !data.city) { previewElement.textContent = "Preencha os campos obrigatórios (Chave, Nome, Cidade) para ver o preview."; return; } try { const payload = this.generatePixPayload(); previewElement.textContent = payload; } catch (e) { previewElement.textContent = "Erro ao gerar payload: " + e.message; } } } validatePixData() { const data = this.collectPixData(); const errors = []; // Required fields if (!data.key.trim()) errors.push('Chave PIX é obrigatória'); if (!data.name.trim()) errors.push('Nome do beneficiário é obrigatório'); if (!data.city.trim()) errors.push('Cidade do beneficiário é obrigatória'); // Length limits if (data.name.length > 25) errors.push('Nome deve ter no máximo 25 caracteres'); if (data.city.length > 15) errors.push('Cidade deve ter no máximo 15 caracteres (Regra do Banco Central)'); // Amount validation if (data.amount) { const amount = parseFloat(data.amount.replace(/\./g, '').replace(',', '.')); if (isNaN(amount) || amount <= 0) { errors.push('Valor deve ser um número maior que zero'); } } // Smart Key Validation const keyWarning = this.validateKeyType(data.keyType, data.key); if (keyWarning) { errors.push(keyWarning); } return errors; } validateKeyType(type, key) { if (!key) return null; const cleanKey = key.replace(/\D/g, ''); // Numbers only // Case 1: Selected CPF/CNPJ if (type === 'cpf') { if (cleanKey.length === 11) { if (!this.isValidCPF(cleanKey)) { // Check if looks like phone (DDD + 9...) if (cleanKey[2] === '9') { return "Selecionado CPF, mas parece um Celular. Altere o tipo para 'Celular' ou corrija."; } return "CPF Inválido. Verifique os dígitos."; } } else if (cleanKey.length === 14) { if (!this.isValidCNPJ(cleanKey)) { return "CNPJ Inválido. Verifique os dígitos."; } } else { if (cleanKey.length > 0) return `CPF/CNPJ deve ter 11 ou 14 números. (Atual: ${cleanKey.length})`; } } // Case 2: Selected Phone if (type === 'phone') { if (cleanKey.length > 0) { if (cleanKey.length !== 10 && cleanKey.length !== 11) { return `Telefone deve ter 10 ou 11 números (DDD + Número). (Atual: ${cleanKey.length})`; } // Smart check: Typed a CPF in Phone field? if (cleanKey.length === 11 && this.isValidCPF(cleanKey)) { // Check if it's NOT a typical mobile number structure // Mobile: DDD (11-99) + 9 + 8 digits. const ddd = parseInt(cleanKey.substring(0, 2)); const ninthDigit = cleanKey[2]; // If DDD is invalid or 9th digit is not 9, but it IS a valid CPF -> Warning if (ddd < 11 || ddd > 99 || ninthDigit !== '9') { return "Isso parece ser um CPF válido, mas o tipo selecionado é 'Celular'."; } } } } // Case 3: Email if (type === 'email') { if (key.length > 0 && !key.includes('@')) { return "Email inválido. Deve conter '@'."; } } return null; } isValidCPF(cpf) { if (typeof cpf !== "string") return false; cpf = cpf.replace(/[\s.-]*/igm, ''); if (cpf.length !== 11 || !Array.from(cpf).filter(e => e !== cpf[0]).length) { return false; } var sum = 0; var remainder; for (var i = 1; i <= 9; i++) sum = sum + parseInt(cpf.substring(i-1, i)) * (11 - i); remainder = (sum * 10) % 11; if ((remainder == 10) || (remainder == 11)) remainder = 0; if (remainder != parseInt(cpf.substring(9, 10)) ) return false; sum = 0; for (i = 1; i <= 10; i++) sum = sum + parseInt(cpf.substring(i-1, i)) * (12 - i); remainder = (sum * 10) % 11; if ((remainder == 10) || (remainder == 11)) remainder = 0; if (remainder != parseInt(cpf.substring(10, 11)) ) return false; return true; } isValidCNPJ(cnpj) { if (!cnpj) return false; cnpj = cnpj.replace(/[^\d]+/g,''); if (cnpj.length != 14) return false; // Elimina CNPJs invalidos conhecidos if (cnpj == "00000000000000" || cnpj == "11111111111111" || cnpj == "22222222222222" || cnpj == "33333333333333" || cnpj == "44444444444444" || cnpj == "55555555555555" || cnpj == "66666666666666" || cnpj == "77777777777777" || cnpj == "88888888888888" || cnpj == "99999999999999") return false; // Valida DVs let tamanho = cnpj.length - 2 let numeros = cnpj.substring(0,tamanho); let digitos = cnpj.substring(tamanho); let soma = 0; let pos = tamanho - 7; for (let i = tamanho; i >= 1; i--) { soma += numeros.charAt(tamanho - i) * pos--; if (pos < 2) pos = 9; } let resultado = soma % 11 < 2 ? 0 : 11 - soma % 11; if (resultado != digitos.charAt(0)) return false; tamanho = tamanho + 1; numeros = cnpj.substring(0,tamanho); soma = 0; pos = tamanho - 7; for (let i = tamanho; i >= 1; i--) { soma += numeros.charAt(tamanho - i) * pos--; if (pos < 2) pos = 9; } resultado = soma % 11 < 2 ? 0 : 11 - soma % 11; if (resultado != digitos.charAt(1)) return false; return true; } collectPixData() { return { keyType: document.getElementById('pix-key-type')?.value || 'cpf', key: document.getElementById('pix-key')?.value || '', name: document.getElementById('pix-name')?.value || '', city: document.getElementById('pix-city')?.value || '', amount: document.getElementById('pix-amount')?.value || '', description: document.getElementById('pix-description')?.value || '', txid: document.getElementById('pix-txid')?.value || '' }; } generateCRC16(payload) { let crc = 0xFFFF; const polynomial = 0x1021; for (let i = 0; i < payload.length; i++) { crc ^= payload.charCodeAt(i) << 8; for (let j = 0; j < 8; j++) { if ((crc & 0x8000) !== 0) { crc = ((crc << 1) ^ polynomial) & 0xFFFF; } else { crc = (crc << 1) & 0xFFFF; } } } return crc.toString(16).toUpperCase().padStart(4, '0'); } formatField(id, value) { const valStr = value.toString(); const len = valStr.length.toString().padStart(2, '0'); return `${id}${len}${valStr}`; } formatKey(type, key) { if (!key) return ''; const cleanKey = key.trim(); switch(type) { case 'phone': // Remove non-digits let nums = cleanKey.replace(/\D/g, ''); // If 10 or 11 digits (DDD+Number), add +55 if ((nums.length === 10 || nums.length === 11) && !nums.startsWith('55')) { return `+55${nums}`; } // If user typed 55..., ensure + if (nums.startsWith('55') && !cleanKey.startsWith('+')) { return `+${nums}`; } // If user typed +..., keep it if (cleanKey.startsWith('+')) { return cleanKey; // Assume correct } return `+${nums}`; // Fallback case 'cpf': case 'cnpj': // Numbers only return cleanKey.replace(/\D/g, ''); case 'email': return cleanKey.toLowerCase(); case 'random': default: return cleanKey; } } generatePixPayload() { const data = this.collectPixData(); // Format key based on type const key = this.formatKey(data.keyType, data.key); const name = this.removeAccents(data.name.trim()).substring(0, 25); const city = this.removeAccents(data.city.trim()).substring(0, 15); // Standard limit 15 chars const amount = data.amount ? parseFloat(data.amount.replace(/\./g, '').replace(',', '.')).toFixed(2) : null; const description = this.removeAccents(data.description.trim()) || ''; const txid = this.removeAccents(data.txid.trim()) || '***'; let payload = this.formatField('00', '01'); const gui = this.formatField('00', 'br.gov.bcb.pix'); const keyField = this.formatField('01', key); let merchantAccountInfo = gui + keyField; if (description) { merchantAccountInfo += this.formatField('02', description); } payload += this.formatField('26', merchantAccountInfo); payload += this.formatField('52', '0000'); // Merchant Category Code payload += this.formatField('53', '986'); // Transaction Currency (BRL) if (amount) { payload += this.formatField('54', amount); // Transaction Amount } payload += this.formatField('58', 'BR'); // Country Code payload += this.formatField('59', name); // Merchant Name payload += this.formatField('60', city); // Merchant City // Field 62: Additional Data Field Template // Subfields: 05 (Reference Label / TxID) const txidField = this.formatField('05', txid); payload += this.formatField('62', txidField); payload += '6304'; // CRC16 ID + Length const crc = this.generateCRC16(payload); return payload + crc; } removeAccents(str) { return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } }