react-isw-webpay
Version:
React integration for Interswitch WebPay payment gateway
380 lines (372 loc) • 12.9 kB
JavaScript
;
var react = require('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] = react.useState(false);
const [error, setError] = react.useState(null);
const webPayServiceRef = react.useRef(new WebPayService());
const initiatePayment = react.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] = react.useState(false);
const timeoutRef = react.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
react.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
react.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,
});
react.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}$/,
};
exports.WebPayModal = WebPayModal;
exports.convertFromMinorUnits = convertFromMinorUnits;
exports.convertToMinorUnits = convertToMinorUnits;
exports.formatAmount = formatAmount;
exports.generateTransactionRef = generateTransactionRef;
exports.useWebPay = useWebPay;
exports.useWebPayModal = useWebPayModal;
exports.validatePaymentRequest = validatePaymentRequest;
exports.validateWebPayConfig = validateWebPayConfig;
exports.validationPatterns = validationPatterns;
//# sourceMappingURL=index.js.map