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) { 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; } } } 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 || '' }; } validatePixData() { const data = this.collectPixData(); const errors = []; 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'); 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)'); if (data.amount) { // Remove thousands separators (.) and replace decimal comma (,) with dot (.) const amount = parseFloat(data.amount.replace(/\./g, '').replace(',', '.')); if (isNaN(amount) || amount <= 0) { errors.push('Valor deve ser um número maior que zero'); } } return errors; } 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 it starts with 55 and is long enough, keep it // 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); // Field 26: Merchant Account Information // Subfields: 00 (GUI), 01 (Key), 02 (Description - optional/standard varies) // Standard EMVCo for PIX puts Description in field 02 of ID 26? // Actually, BR Code standard puts "InfoAdicional" in Field 26, ID 02. 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, ""); } }