UNPKG

react-isw-webpay

Version:

React integration for Interswitch WebPay payment gateway

369 lines (362 loc) 12.6 kB
import { useState, useRef, useCallback, useEffect } from 'react'; class WebPayService { constructor() { this.scriptLoadPromise = null; } async loadScript(paymentRequest) { console.log('Loading WebPay script...'); // Return existing promise if script is already loading if (this.scriptLoadPromise) { console.log('Script already loading, returning existing promise'); return this.scriptLoadPromise; } // If script is already loaded, resolve immediately if (typeof window !== 'undefined' && window.webpayCheckout) { console.log('WebPay script already loaded'); return Promise.resolve(); } console.log('Creating new script element'); this.scriptLoadPromise = new Promise((resolve, reject) => { const script = document.createElement('script'); // Always use the live URL as per your working HTML example const scriptUrl = paymentRequest.scriptUrl || 'https://newwebpay.interswitchng.com/inline-checkout.js'; console.log('Script URL:', scriptUrl); script.src = scriptUrl; script.async = true; script.onload = () => { console.log('Script loaded successfully'); // Give the script time to initialize setTimeout(() => { if (window.webpayCheckout) { console.log('webpayCheckout function is available'); resolve(); } else { console.error('webpayCheckout function not found after script load'); reject(new Error('WebPay script failed to initialize - webpayCheckout not found')); } }, 1000); }; script.onerror = (error) => { console.error('Script loading failed:', error); reject(new Error(`Failed to load WebPay script: ${error}`)); }; // Remove existing script if any const existingScript = document.querySelector('script[src*="inline-checkout.js"]'); if (existingScript) { console.log('Removing existing script'); existingScript.remove(); } console.log('Appending script to head'); document.head.appendChild(script); }); return this.scriptLoadPromise; } async initiatePayment(paymentRequest) { try { console.log('=== INITIATING PAYMENT ==='); console.log('Payment request:', paymentRequest); await this.loadScript(paymentRequest); if (!window.webpayCheckout) { throw new Error('WebPay checkout function not available'); } console.log('Calling webpayCheckout with:', paymentRequest); // Call the WebPay function window.webpayCheckout(paymentRequest); console.log('webpayCheckout called successfully'); } catch (error) { console.error('=== PAYMENT INITIATION FAILED ==='); console.error('Error:', error); throw error; } } } const useWebPay = () => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const webPayServiceRef = useRef(new WebPayService()); const initiatePayment = useCallback(async (paymentRequest, options) => { setIsLoading(true); setError(null); try { const paymentData = { ...paymentRequest, onComplete: (response) => { console.log('Payment response:', response); setIsLoading(false); if (response.resp === '00' || response.resp === 'Approved') { options?.onSuccess?.(response); } else if (response.resp === 'Z6') { options?.onCancel?.(); } else { options?.onError?.(response); } } }; console.log('Starting payment with data:', paymentData); await webPayServiceRef.current.initiatePayment(paymentData); } catch (err) { console.error('Payment error:', err); setIsLoading(false); const errorMessage = err instanceof Error ? err.message : 'Payment initialization failed'; setError(errorMessage); options?.onError?.({ resp: 'ERROR', desc: errorMessage, txnref: paymentRequest.txn_ref, payRef: '', retRef: '', amount: paymentRequest.amount, apprAmt: 0 }); } }, []); return { initiatePayment, isLoading, error, clearError: () => setError(null) }; }; // hooks/index.ts const useWebPayModal = ({ isOpen, onClose, paymentRequest, options, }) => { const { initiatePayment, isLoading, error } = useWebPay(); const [hasInitiated, setHasInitiated] = useState(false); const timeoutRef = useRef(); const enhancedOptions = { onSuccess: (response) => { console.log('Payment successful:', response); options?.onSuccess?.(response); onClose(); }, onError: (error) => { console.log('Payment error:', error); options?.onError?.(error); }, onCancel: () => { console.log('Payment cancelled'); options?.onCancel?.(); onClose(); }, }; const handleInitiatePayment = () => { if (!hasInitiated && !isLoading) { console.log('Initiating payment...', paymentRequest); setHasInitiated(true); initiatePayment(paymentRequest, enhancedOptions); } }; const reset = () => { setHasInitiated(false); if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; // Auto-initiate payment when modal opens with a slight delay useEffect(() => { if (isOpen && !hasInitiated && !isLoading) { // Small delay to ensure DOM is ready timeoutRef.current = setTimeout(() => { handleInitiatePayment(); }, 100); } return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, [isOpen, hasInitiated, isLoading]); // Reset state when modal closes useEffect(() => { if (!isOpen) { setHasInitiated(false); if (timeoutRef.current) { clearTimeout(timeoutRef.current); } } }, [isOpen]); return { isLoading, error, hasInitiated, initiatePayment: handleInitiatePayment, reset, close: onClose, }; }; const WebPayModal = ({ isOpen, onClose, // config, paymentRequest, options, }) => { useWebPayModal({ isOpen, onClose, // config, paymentRequest, options, }); useEffect(() => { if (!isOpen) return; // 1. First try immediate modification modifyOverlay(); // 2. Set up mutation observer for dynamic iframe content const observer = new MutationObserver(() => { modifyOverlay(); }); observer.observe(document.body, { childList: true, subtree: true }); return () => observer.disconnect(); }, [isOpen]); const modifyOverlay = () => { const selectors = [ '.overlay-class', '.modal-overlay', '[class*="overlay"]', 'div[style*="background-color: rgba(0, 0, 0"]' ]; selectors.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(el => { el.style.cssText = ` background-color: transparent !important; pointer-events: none !important; z-index: -1 !important; `; }); }); }; return null; }; /** * Creates a WebPay configuration object with environment-based settings */ /** * Validates a payment request object */ const validatePaymentRequest = (request) => { const errors = []; // Required fields validation if (!request.txnRef || request.txnRef.trim().length === 0) { errors.push('Transaction reference (txnRef) is required'); } if (!request.amount || request.amount <= 0) { errors.push('Amount must be greater than 0'); } // Optional but recommended validations if (request.txnRef && request.txnRef.length > 50) { errors.push('Transaction reference should not exceed 50 characters'); } if (request.amount && request.amount > 999999999) { errors.push('Amount exceeds maximum allowed value'); } // Email validation if custId looks like an email if (request.custId && request.custId.includes('@')) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(request.custId)) { errors.push('Customer ID appears to be an email but is not valid'); } } // Currency validation if (request.currency && !['566', '840', '978'].includes(String(request.currency))) { console.warn('Currency code may not be supported. Common codes: 566 (NGN), 840 (USD), 978 (EUR)'); } return { isValid: errors.length === 0, errors }; }; /** * Validates WebPay configuration */ const validateWebPayConfig = (config) => { const errors = []; if (!config.merchantCode || config.merchantCode.trim().length === 0) { errors.push('Merchant code is required'); } if (!config.payItemId || config.payItemId.trim().length === 0) { errors.push('Pay item ID is required'); } if (!['TEST', 'LIVE'].includes(config.mode)) { errors.push('Mode must be either TEST or LIVE'); } if (config.scriptUrl && !isValidUrl(config.scriptUrl)) { errors.push('Script URL must be a valid URL'); } return { isValid: errors.length === 0, errors }; }; /** * Formats amount for display */ const formatAmount = (amount, currency = '566', locale = 'en-NG') => { const currencyMap = { '566': 'NGN', '840': 'USD', '978': 'EUR' }; const currencyCode = currencyMap[currency] || 'NGN'; try { return new Intl.NumberFormat(locale, { style: 'currency', currency: currencyCode, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount / 100); // Assuming amount is in kobo/cents } catch (error) { // Fallback formatting const symbol = currencyCode === 'NGN' ? '₦' : '$'; return `${symbol}${(amount / 100).toLocaleString()}`; } }; /** * Generates a unique transaction reference */ const generateTransactionRef = (prefix = 'txn') => { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); return `${prefix}_${timestamp}_${random}`; }; /** * Converts amount to kobo (for NGN) or cents (for other currencies) */ const convertToMinorUnits = (amount, currency = '566') => { // Nigerian Naira uses kobo (1 NGN = 100 kobo) // Most other currencies use similar 100:1 ratio if (currency === '566') { return Math.round(amount * 100); // Convert NGN to kobo } return Math.round(amount * 100); }; /** * Converts from kobo/cents back to major units */ const convertFromMinorUnits = (amount) => { return amount / 100; }; // Helper function for URL validation const isValidUrl = (string) => { try { new URL(string); return true; } catch (_) { return false; } }; // Export common validation regex patterns const validationPatterns = { email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, phone: /^\+?[\d\s\-\(\)]+$/, transactionRef: /^[a-zA-Z0-9_\-]{1,50}$/, }; export { WebPayModal, convertFromMinorUnits, convertToMinorUnits, formatAmount, generateTransactionRef, useWebPay, useWebPayModal, validatePaymentRequest, validateWebPayConfig, validationPatterns }; //# sourceMappingURL=index.esm.js.map