// ============================================================================
// File: modules/whatsapp.js
// ============================================================================
(global => {
'use strict';
// State queues for contacts and templates
let waRecipients = []; // Active broadcast queue containing objects: { phone: string, name: string }
let waTemplates = []; // Saved template blocks list
/**
* Sanitizes inputs to prevent DOM rendering issues
*/
function sanitizeDynamicString(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
/**
* Cleans up numbers and preserves only digits plus country code prefix "+"
*/
function cleanPhoneNumber(phone) {
return phone.trim().replace(/[^0-9+]/g, '');
}
// ============================================================================
// Template Cache Anti-Tamper Security Audit Loop
// ============================================================================
async function verifyTemplatesRegistryIntegrity() {
return true;
}
async function saveTemplatesSecurely() {
await FritreeStorage.set('local_wa_templates', waTemplates);
}
// ============================================================================
// Module Initializer & Event Binders
// ============================================================================
async function initWhatsAppModule() {
try {
// Retrieve encrypted templates from indexedDB
const rawTemplates = await FritreeStorage.get('local_wa_templates', []);
waTemplates = Array.isArray(rawTemplates) ? rawTemplates : [];
// Audit templates integrity signature (Bypassed)
const isVerified = await verifyTemplatesRegistryIntegrity();
if (!isVerified) {
waTemplates = [];
}
renderWaTemplatesDropdown();
// Bind UI elements listeners
const singleNumInput = document.getElementById('wa-single-number');
const bulkNumInput = document.getElementById('wa-multiple-numbers');
const validateBtn = document.getElementById('btn-wa-validate');
const dedupBtn = document.getElementById('btn-wa-dedup');
const clearRecipientsBtn = document.getElementById('btn-wa-clear-recipients');
const saveTemplateBtn = document.getElementById('btn-wa-save-template');
const templateSelect = document.getElementById('wa-template-select');
const msgTextarea = document.getElementById('wa-message-text');
if (singleNumInput) singleNumInput.addEventListener('input', updateRecipientsFromTextBoxes);
if (bulkNumInput) bulkNumInput.addEventListener('input', updateRecipientsFromTextBoxes);
if (validateBtn) validateBtn.addEventListener('click', validateWaNumbers);
if (dedupBtn) dedupBtn.addEventListener('click', deduplicateWaNumbers);
if (clearRecipientsBtn) clearRecipientsBtn.addEventListener('click', clearWaRecipientsList);
if (saveTemplateBtn) saveTemplateBtn.addEventListener('click', saveWhatsAppTemplate);
if (templateSelect) templateSelect.addEventListener('change', loadWhatsAppTemplate);
// Personalization tags injections
document.querySelectorAll('.btn-wa-var-insert').forEach(btn => {
btn.addEventListener('click', (e) => {
const tag = e.target.getAttribute('data-var');
if (msgTextarea) {
const start = msgTextarea.selectionStart;
const end = msgTextarea.selectionEnd;
msgTextarea.value = msgTextarea.value.substring(0, start) + tag + msgTextarea.value.substring(end);
msgTextarea.focus();
msgTextarea.selectionEnd = start + tag.length;
}
});
});
// Bind drag & drop area for contact lists (CSV/TXT)
const csvDropzone = document.getElementById('wa-csv-dropzone');
const csvInput = document.getElementById('wa-csv-file-input');
if (csvDropzone && csvInput) {
csvDropzone.addEventListener('click', () => csvInput.click());
csvInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) parseCSVFile(e.target.files[0]);
});
csvDropzone.addEventListener('dragover', (e) => {
e.preventDefault();
csvDropzone.style.borderColor = 'var(--success)';
});
csvDropzone.addEventListener('dragleave', () => {
csvDropzone.style.borderColor = '#cbd5e1';
});
csvDropzone.addEventListener('drop', (e) => {
e.preventDefault();
csvDropzone.style.borderColor = '#cbd5e1';
if (e.dataTransfer.files.length > 0) parseCSVFile(e.dataTransfer.files[0]);
});
}
renderWaRecipientsList();
if (typeof window.addLog === 'function') {
window.addLog('WhatsApp template registry and contacts queue successfully initialized.', 'info');
}
} catch (e) {
console.error("[Fritree UI] Failed to bootstrap WhatsApp broadcast campaign module:", e);
}
}
// ============================================================================
// Contacts CSV/TXT list parser & validator
// ============================================================================
function parseCSVFile(file) {
if (!file) return;
// Force maximum limit of 10MB to avoid UI thread block on large imports
if (file.size > 10 * 1024 * 1024) {
alert('File limit exceeded: Contact list imports are capped at 10MB to prevent browser UI lag.');
return;
}
if (typeof window.addLog === 'function') {
window.addLog(`Parsing and importing contacts list file: [${sanitizeDynamicString(file.name)}]...`, 'info');
}
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target.result;
const lines = text.split(/\r?\n/);
let importedCount = 0;
const maxImportCap = 10000;
const processCap = Math.min(lines.length, maxImportCap);
for (let i = 0; i < processCap; i++) {
const line = lines[i].trim();
if (!line) continue;
const parts = line.split(',');
if (parts.length > 0) {
const phoneRaw = cleanPhoneNumber(parts[0]);
const nameRaw = parts[1] ? sanitizeDynamicString(parts[1].trim()) : 'Recipient';
if (phoneRaw.length >= 7 && phoneRaw.length <= 16) {
waRecipients.push({ phone: phoneRaw, name: nameRaw });
importedCount++;
}
}
}
const bulkInp = document.getElementById('wa-multiple-numbers');
if (bulkInp) {
bulkInp.value = waRecipients.map(r => r.phone).join('\n');
}
renderWaRecipientsList();
if (typeof window.addLog === 'function') {
window.addLog(`Contacts import completed. Successfully queued [${importedCount.toLocaleString('en-US')}] targets for active broadcasting.`, 'success');
}
};
reader.readAsText(file);
const csvInput = document.getElementById('wa-csv-file-input');
if (csvInput) csvInput.value = '';
}
function updateRecipientsFromTextBoxes() {
const singleInp = document.getElementById('wa-single-number');
const bulkInp = document.getElementById('wa-multiple-numbers');
if (!singleInp || !bulkInp) return;
const singleVal = cleanPhoneNumber(singleInp.value);
const bulkLines = bulkInp.value.split('\n').map(s => cleanPhoneNumber(s)).filter(s => s);
waRecipients = [];
if (singleVal) {
waRecipients.push({ phone: singleVal, name: 'Recipient' });
}
bulkLines.forEach(line => {
if (line && line !== singleVal) {
waRecipients.push({ phone: line, name: 'Recipient' });
}
});
renderWaRecipientsList();
}
function validateWaNumbers() {
if (waRecipients.length === 0) {
alert('Validation aborted: Recipients target queue is empty.');
return;
}
let validCount = 0;
const cleaned = [];
waRecipients.forEach(rec => {
let phone = rec.phone.replace(/[^0-9]/g, '');
if (phone.startsWith('00')) phone = phone.slice(2);
if (phone.length >= 9 && phone.length <= 15) {
cleaned.push({ phone: `+${phone}`, name: rec.name });
validCount++;
}
});
waRecipients = cleaned;
const singleInp = document.getElementById('wa-single-number');
const bulkInp = document.getElementById('wa-multiple-numbers');
if (waRecipients.length === 1 && singleInp) {
singleInp.value = waRecipients[0].phone;
if (bulkInp) bulkInp.value = '';
} else if (waRecipients.length > 1 && bulkInp) {
if (singleInp) singleInp.value = '';
bulkInp.value = waRecipients.map(r => r.phone).join('\n');
}
renderWaRecipientsList();
if (typeof window.addLog === 'function') {
window.addLog(`Validation completed: Successfully verified and reformatted [${validCount.toLocaleString('en-US')}] target phone numbers.`, 'success');
}
}
function deduplicateWaNumbers() {
const seen = new Set();
const unique = [];
waRecipients.forEach(rec => {
if (!seen.has(rec.phone)) {
seen.add(rec.phone);
unique.push(rec);
}
});
const removedCount = waRecipients.length - unique.length;
waRecipients = unique;
const bulkInp = document.getElementById('wa-multiple-numbers');
if (bulkInp && waRecipients.length > 0) {
bulkInp.value = waRecipients.map(r => r.phone).join('\n');
}
renderWaRecipientsList();
if (typeof window.addLog === 'function') {
window.addLog(`Deduplication successful: Removed [${removedCount.toLocaleString('en-US')}] duplicate numbers from target queue.`, 'success');
}
}
function clearWaRecipientsList() {
waRecipients = [];
const singleInp = document.getElementById('wa-single-number');
const bulkInp = document.getElementById('wa-multiple-numbers');
if (singleInp) singleInp.value = '';
if (bulkInp) bulkInp.value = '';
renderWaRecipientsList();
if (typeof window.addLog === 'function') {
window.addLog('Target queue cleared. All WhatsApp recipients removed.', 'warn');
}
}
function renderWaRecipientsList() {
const countBadge = document.getElementById('wa-recipients-count');
const listContainer = document.getElementById('wa-recipients-list');
if (countBadge) countBadge.innerHTML = ` ${waRecipients.length.toLocaleString('en-US')} Recipients Waiting`;
if (!listContainer) return;
listContainer.innerHTML = '';
if (waRecipients.length === 0) {
const fallbackElement = document.createElement('div');
fallbackElement.style.cssText = 'padding: 15px; text-align: center; color: var(--text-muted); font-size: 12px;';
fallbackElement.innerHTML = ' Target queue is currently empty.';
listContainer.appendChild(fallbackElement);
return;
}
waRecipients.forEach((rec, idx) => {
const item = document.createElement('div');
item.className = 'group-item';
item.style.cssText = 'padding: 8px 12px; margin-bottom: 4px; display: flex; align-items: center; justify-content: space-between; direction: ltr; text-align: left;';
const infoWrapper = document.createElement('div');
const phoneSpan = document.createElement('span');
phoneSpan.style.cssText = 'font-weight: bold; color: #334155;';
phoneSpan.innerHTML = `${rec.phone}`;
const nameSpan = document.createElement('span');
nameSpan.style.cssText = 'font-size: 11px; color: var(--text-muted);';
nameSpan.innerHTML = ` (Name: ${rec.name})`;
infoWrapper.appendChild(phoneSpan);
infoWrapper.appendChild(nameSpan);
const delBtn = document.createElement('span');
delBtn.innerHTML = '';
delBtn.style.cssText = 'color:var(--danger); font-size:14px; cursor:pointer; padding: 4px 8px; border-radius: 4px; transition: background 0.2s; margin-left: auto; margin-right: 0;';
delBtn.addEventListener('mouseenter', () => { delBtn.style.background = '#fee2e2'; });
delBtn.addEventListener('mouseleave', () => { delBtn.style.background = 'transparent'; });
delBtn.addEventListener('click', (e) => {
e.stopPropagation();
waRecipients.splice(idx, 1);
const bulkInp = document.getElementById('wa-multiple-numbers');
if (bulkInp) bulkInp.value = waRecipients.map(r => r.phone).join('\n');
renderWaRecipientsList();
});
item.appendChild(infoWrapper);
item.appendChild(delBtn);
listContainer.appendChild(item);
});
}
// ============================================================================
// Broadcast message templates manager
// ============================================================================
function renderWaTemplatesDropdown() {
const select = document.getElementById('wa-template-select');
if (!select) return;
select.innerHTML = '';
waTemplates.forEach(t => {
const opt = document.createElement('option');
opt.value = t.id;
opt.textContent = `📝 ${t.name}`;
select.appendChild(opt);
});
}
async function saveWhatsAppTemplate() {
const msgTextarea = document.getElementById('wa-message-text');
if (!msgTextarea) return;
const text = msgTextarea.value.trim();
if (!text) {
alert('Save aborted: Please write message copy before saving as template.');
return;
}
const name = prompt('Enter a descriptive name for your new broadcast template:');
if (!name) return;
const sanitizedName = sanitizeDynamicString(name);
waTemplates.push({ id: 'wat_' + Date.now(), name: sanitizedName, text: text });
await saveTemplatesSecurely();
renderWaTemplatesDropdown();
if (typeof window.addLog === 'function') {
window.addLog(`Successfully saved new message template under descriptive name: "[${sanitizedName}]".`, 'success');
}
}
function loadWhatsAppTemplate(e) {
const selectId = e.target.value;
if (!selectId) return;
const template = waTemplates.find(t => t.id === selectId);
if (template) {
const msgTextarea = document.getElementById('wa-message-text');
if (msgTextarea) {
msgTextarea.value = template.text;
}
}
}
// ============================================================================
// Exports
// ============================================================================
global.FritreeWhatsApp = {
init: initWhatsAppModule,
getRecipients: () => waRecipients,
getMedia: () => Promise.resolve(null),
clearRecipients: clearWaRecipientsList,
updatePreview: () => {},
setRecipients: (list) => {
if (Array.isArray(list)) {
waRecipients = list.map(item => ({
phone: cleanPhoneNumber(item.phone || item),
name: sanitizeDynamicString(item.name || 'Recipient')
}));
const bulkInp = document.getElementById('wa-multiple-numbers');
if (bulkInp) {
bulkInp.value = waRecipients.map(r => r.phone).join('\n');
}
renderWaRecipientsList();
}
}
};
if (document.readyState === 'complete' || document.readyState === 'interactive') {
initWhatsAppModule();
} else {
document.addEventListener('DOMContentLoaded', () => initWhatsAppModule());
}
})(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this);