UNPKG

venepagos

Version:

SDK oficial de VenePagos para integrar pagos en aplicaciones web

462 lines (398 loc) 14.1 kB
/** * SDK oficial de VenePagos para integrar pagos en aplicaciones web * @version 1.0.0 * @author VenePagos */ class VenePagosSDK { /** * Inicializa el SDK de VenePagos * @param {Object} config - Configuración del SDK * @param {string} config.apiKey - API Key de VenePagos (debe comenzar con 'vp_') * @param {string} [config.baseUrl='https://www.venepagos.com.ve'] - URL base de la API * @param {boolean} [config.sandbox=false] - Usar entorno de pruebas */ constructor(config) { if (!config || !config.apiKey) { throw new Error('API Key es requerido para inicializar VenePagos SDK'); } if (!config.apiKey.startsWith('vp_')) { throw new Error('API Key debe comenzar con "vp_"'); } this.apiKey = config.apiKey; this.baseUrl = config.baseUrl || 'https://www.venepagos.com.ve'; this.sandbox = config.sandbox || false; // Estado interno para manejar ventanas de pago this.activePaymentWindows = new Map(); this.eventListeners = []; // Configurar listener para mensajes de ventanas emergentes this._setupMessageListener(); } /** * Crea un nuevo enlace de pago * @param {Object} paymentData - Datos del pago * @param {string} paymentData.title - Título del pago (mínimo 3 caracteres) * @param {string} [paymentData.description] - Descripción del pago * @param {number} [paymentData.amount] - Monto del pago (opcional para monto variable) * @param {string} [paymentData.currency='USD'] - Moneda (USD, VES) * @param {string} [paymentData.expiresAt] - Fecha de expiración (ISO 8601) * @returns {Promise<Object>} - Datos del payment link creado */ async createPaymentLink(paymentData) { try { const response = await fetch(`${this.baseUrl}/api/public/payment-links/create`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` }, body: JSON.stringify({ title: paymentData.title, description: paymentData.description, amount: paymentData.amount, currency: paymentData.currency || 'USD', expiresAt: paymentData.expiresAt }) }); const result = await response.json(); if (!response.ok || !result.success) { throw new Error(result.error || `Error HTTP ${response.status}`); } return result.data; } catch (error) { throw new Error(`Error al crear payment link: ${error.message}`); } } /** * Abre una ventana emergente con el enlace de pago * @param {string} paymentUrl - URL del payment link * @param {Object} [options] - Opciones de la ventana * @param {number} [options.width=600] - Ancho de la ventana * @param {number} [options.height=700] - Alto de la ventana * @param {boolean} [options.centered=true] - Centrar la ventana * @returns {Promise<Object>} - Promise que se resuelve cuando el pago es completado */ async openPaymentPopup(paymentUrl, options = {}) { return new Promise((resolve, reject) => { const windowOptions = { width: options.width || 600, height: options.height || 700, centered: options.centered !== false }; // Calcular posición centrada let left = 0; let top = 0; if (windowOptions.centered) { left = Math.round((window.screen.width - windowOptions.width) / 2); top = Math.round((window.screen.height - windowOptions.height) / 2); } const windowFeatures = [ `width=${windowOptions.width}`, `height=${windowOptions.height}`, `left=${left}`, `top=${top}`, 'scrollbars=yes', 'resizable=yes', 'status=no', 'toolbar=no', 'menubar=no', 'location=no' ].join(','); // Abrir ventana emergente const popup = window.open(paymentUrl, 'venepagos_payment', windowFeatures); if (!popup) { reject(new Error('No se pudo abrir la ventana emergente. Verifica que las ventanas emergentes estén habilitadas.')); return; } // Generar ID único para esta ventana de pago const paymentId = this._generatePaymentId(); // Registrar la ventana activa this.activePaymentWindows.set(paymentId, { window: popup, resolve, reject, url: paymentUrl, timestamp: Date.now() }); // Monitorear si la ventana es cerrada manualmente const checkClosed = setInterval(() => { if (popup.closed) { clearInterval(checkClosed); const paymentData = this.activePaymentWindows.get(paymentId); if (paymentData) { this.activePaymentWindows.delete(paymentId); // Verificar si hay alguna referencia de pago guardada antes de rechazar const savedReference = localStorage.getItem('lastPaymentReference'); if (savedReference) { localStorage.removeItem('lastPaymentReference'); resolve({ success: true, reference: savedReference, status: 'completed', closedManually: true }); } else { reject(new Error('Ventana de pago cerrada por el usuario')); } } } }, 1000); // Timeout de seguridad (30 minutos) setTimeout(() => { if (this.activePaymentWindows.has(paymentId)) { clearInterval(checkClosed); popup.close(); this.activePaymentWindows.delete(paymentId); reject(new Error('Timeout: El pago excedió el tiempo límite')); } }, 30 * 60 * 1000); }); } /** * Crea un payment link y abre inmediatamente la ventana de pago * @param {Object} paymentData - Datos del pago * @param {Object} [popupOptions] - Opciones de la ventana emergente * @returns {Promise<Object>} - Promise que se resuelve cuando el pago es completado */ async createAndOpenPayment(paymentData, popupOptions = {}) { try { // Crear el payment link const paymentLink = await this.createPaymentLink(paymentData); // Abrir la ventana de pago const paymentResult = await this.openPaymentPopup(paymentLink.url, popupOptions); return { paymentLink, paymentResult }; } catch (error) { throw new Error(`Error en el proceso de pago: ${error.message}`); } } /** * Obtiene información de un payment link existente * @param {string} paymentLinkId - ID del payment link * @returns {Promise<Object>} - Información del payment link */ async getPaymentLink(paymentLinkId) { try { const response = await fetch(`${this.baseUrl}/api/user/payment-links/${paymentLinkId}`, { headers: { 'Authorization': `Bearer ${this.apiKey}` } }); const result = await response.json(); if (!response.ok || !result.success) { throw new Error(result.error || `Error HTTP ${response.status}`); } return result.data; } catch (error) { throw new Error(`Error al obtener payment link: ${error.message}`); } } /** * Lista todos los payment links del usuario * @param {Object} [options] - Opciones de filtrado * @param {number} [options.limit=50] - Cantidad máxima de resultados * @param {number} [options.offset=0] - Desplazamiento para paginación * @param {boolean} [options.isActive] - Filtrar por estado activo * @returns {Promise<Object>} - Lista de payment links con información de paginación */ async listPaymentLinks(options = {}) { try { const params = new URLSearchParams(); if (options.limit) params.append('limit', options.limit.toString()); if (options.offset) params.append('offset', options.offset.toString()); if (options.isActive !== undefined) params.append('isActive', options.isActive.toString()); const url = `${this.baseUrl}/api/public/payment-links/list${params.toString() ? '?' + params.toString() : ''}`; const response = await fetch(url, { headers: { 'Authorization': `Bearer ${this.apiKey}` } }); const result = await response.json(); if (!response.ok || !result.success) { throw new Error(result.error || `Error HTTP ${response.status}`); } return result.data; } catch (error) { throw new Error(`Error al listar payment links: ${error.message}`); } } /** * Configura un listener para eventos de pago * @param {string} event - Tipo de evento ('success', 'error', 'cancel') * @param {Function} callback - Función a ejecutar cuando ocurra el evento */ on(event, callback) { if (typeof callback !== 'function') { throw new Error('El callback debe ser una función'); } this.eventListeners.push({ event, callback }); } /** * Remueve un listener de eventos * @param {string} event - Tipo de evento * @param {Function} callback - Función a remover */ off(event, callback) { this.eventListeners = this.eventListeners.filter( listener => !(listener.event === event && listener.callback === callback) ); } /** * Cierra todas las ventanas de pago activas */ closeAllPaymentWindows() { this.activePaymentWindows.forEach((paymentData) => { if (paymentData.window && !paymentData.window.closed) { paymentData.window.close(); } }); this.activePaymentWindows.clear(); } /** * Valida una API key * @returns {Promise<boolean>} - true si la API key es válida */ async validateApiKey() { try { const response = await fetch(`${this.baseUrl}/api/public/test-api-key`, { headers: { 'Authorization': `Bearer ${this.apiKey}` } }); return response.ok; } catch { return false; } } // Métodos privados _setupMessageListener() { const messageHandler = (event) => { // Verificar que el mensaje venga del dominio correcto if (!event.origin.includes('venepagos')) return; const { type, data } = event.data || {}; // Manejar diferentes tipos de mensajes switch (type) { case 'PAYMENT_SUCCESS': this._handlePaymentSuccess(data); break; case 'PAYMENT_ERROR': this._handlePaymentError(data); break; case 'PAYMENT_CANCEL': this._handlePaymentCancel(data); break; } }; window.addEventListener('message', messageHandler); // Guardar referencia para poder remover el listener después this._messageHandler = messageHandler; } _handlePaymentSuccess(data) { // Emitir evento de éxito this._emitEvent('success', data); // Resolver promesas de ventanas activas si es necesario this.activePaymentWindows.forEach((paymentData, paymentId) => { if (paymentData.resolve) { paymentData.resolve({ success: true, reference: data.reference, status: 'completed', data }); this.activePaymentWindows.delete(paymentId); } }); } _handlePaymentError(data) { this._emitEvent('error', data); this.activePaymentWindows.forEach((paymentData, paymentId) => { if (paymentData.reject) { paymentData.reject(new Error(data.error || 'Error en el pago')); this.activePaymentWindows.delete(paymentId); } }); } _handlePaymentCancel(data) { this._emitEvent('cancel', data); this.activePaymentWindows.forEach((paymentData, paymentId) => { if (paymentData.reject) { paymentData.reject(new Error('Pago cancelado por el usuario')); this.activePaymentWindows.delete(paymentId); } }); } _emitEvent(event, data) { this.eventListeners .filter(listener => listener.event === event) .forEach(listener => { try { listener.callback(data); } catch (error) { console.error('Error en callback de evento:', error); } }); } _generatePaymentId() { return `payment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Limpia recursos del SDK */ destroy() { // Cerrar todas las ventanas activas this.closeAllPaymentWindows(); // Remover event listeners if (this._messageHandler) { window.removeEventListener('message', this._messageHandler); } // Limpiar referencias this.eventListeners = []; this.activePaymentWindows.clear(); } } // Funciones de utilidad estáticas VenePagosSDK.utils = { /** * Formatea un monto para mostrar * @param {number} amount - Monto * @param {string} currency - Moneda * @returns {string} - Monto formateado */ formatAmount(amount, currency = 'USD') { if (typeof amount !== 'number') return '0.00'; const formatter = new Intl.NumberFormat('es-VE', { style: 'currency', currency: currency, minimumFractionDigits: 2, maximumFractionDigits: 2 }); return formatter.format(amount); }, /** * Valida si una fecha de expiración es válida * @param {string} expiresAt - Fecha en formato ISO * @returns {boolean} - true si es válida y futura */ isValidExpiryDate(expiresAt) { if (!expiresAt) return false; const date = new Date(expiresAt); return !isNaN(date.getTime()) && date > new Date(); }, /** * Genera una fecha de expiración * @param {number} hours - Horas desde ahora * @returns {string} - Fecha en formato ISO */ generateExpiryDate(hours = 24) { const date = new Date(); date.setHours(date.getHours() + hours); return date.toISOString(); } }; // Exportar para diferentes entornos if (typeof module !== 'undefined' && module.exports) { module.exports = VenePagosSDK; } else if (typeof window !== 'undefined') { window.VenePagosSDK = VenePagosSDK; }