/** * Easy Invoice Payment Handler * * @package EasyInvoice * @since 1.0.0 */ (function($) { 'use strict'; // Module pattern for better encapsulation const EasyInvoicePayment = { // Properties initialized: false, selectedGateway: null, stripeInstance: null, cardElement: null, stripeClientSecret: null, $paymentButton: null, originalPayButtonText: '', messageClearTimeout: null, $paymentPanel: null, $mainContentArea: null, $openPaymentPanelButton: null, $closePaymentPanelButton: null, /** * Initialize the payment handler */ init: function() { // Prevent multiple initializations if (window.easyInvoicePaymentInitialized || this.initialized) { return; } this.initialized = true; window.easyInvoicePaymentInitialized = true; // Cache DOM elements this.$paymentButton = $('.payment-button'); this.originalPayButtonText = this.$paymentButton.text() || 'Pay Now'; this.$paymentPanel = $('#payment-slideout-panel'); this.$mainContentArea = $('.easy-invoice-main-content-area'); this.$openPaymentPanelButton = $('#open-payment-panel-button'); this.$closePaymentPanelButton = $('#close-payment-panel-button'); // Setup event handlers this.setupEventListeners(); // Show default payment method if pre-selected const $defaultMethod = $('input[name="payment_method_radio"]:checked'); if ($defaultMethod.length) { $defaultMethod.trigger('change'); } }, /** * Setup all event listeners */ setupEventListeners: function() { // Payment method selection $('input[name="payment_method_radio"]').on('change', this.handlePaymentMethodChange.bind(this)); // Payment button this.$paymentButton.on('click', this.handlePaymentButtonClick.bind(this)); // Panel open/close this.$openPaymentPanelButton.on('click', this.handleOpenPanel.bind(this)); this.$closePaymentPanelButton.on('click', this.handleClosePanel.bind(this)); // Close panel with Escape key $(document).on('keydown', this.handleKeyDown.bind(this)); }, /** * Handle payment method selection change */ handlePaymentMethodChange: function(e) { this.selectedGateway = $(e.currentTarget).val(); const $selectedEntry = $(e.currentTarget).closest('.payment-method-entry'); // Update UI $('.payment-method-entry').removeClass('selected-gateway border-indigo-600 shadow-lg') .addClass('border-gray-300 shadow-sm'); $selectedEntry.addClass('selected-gateway border-indigo-600 shadow-lg') .removeClass('border-gray-300 shadow-sm'); // Hide all payment form areas $('.easy-invoice-stripe-card-area').addClass('hidden'); // Clear card errors if any if (this.cardElement) { const $cardErrorsDiv = $('#card-errors'); if ($cardErrorsDiv.length) { $cardErrorsDiv.text(''); } } // Reset payment button this.$paymentButton.text(this.originalPayButtonText); // Handle Stripe gateway if (this.selectedGateway === 'stripe') { this.initializeStripePayment($selectedEntry); } else { // For other gateways, enable payment button immediately this.$paymentButton.prop('disabled', false); } }, /** * Initialize Stripe payment elements */ initializeStripePayment: function($selectedEntry) { const $stripeCardArea = $selectedEntry.find('#stripe-card-area'); if (!$stripeCardArea.length) { // Stripe card area not found this.showPaymentMessage('Stripe UI setup error. Please try refreshing.', 'error'); return; } $stripeCardArea.removeClass('hidden').html('
Loading card details...
'); const invoiceId = this.$paymentButton.data('invoice-id'); if (!invoiceId) { $stripeCardArea.html('Error: Invoice ID missing.
'); return; } // Request client secret from server $.ajax({ url: easy_invoice_vars.ajax_url, type: 'POST', data: { action: 'easy_invoice_process_payment', invoice_id: invoiceId, payment_method: 'stripe', payment_nonce: easy_invoice_vars.nonce }, success: (response) => { if (response.success && response.data.client_secret) { this.stripeClientSecret = response.data.client_secret; // Setup card element HTML $stripeCardArea.html(` `); if (this.initializeStripeElements(this.stripeClientSecret, $selectedEntry)) { this.$paymentButton.text('Pay with Card'); // Set button state based on card completeness const currentCardState = this.cardElement._empty ? false : this.cardElement._complete; this.$paymentButton.prop('disabled', !currentCardState); } else { this.$paymentButton.prop('disabled', true); } } else { const errorMsg = response.data && response.data.message ? response.data.message : 'Could not initialize Stripe payment.'; $stripeCardArea.html(`${errorMsg}
`); this.$paymentButton.prop('disabled', true); } }, error: (xhr) => { // Stripe client_secret AJAX error $stripeCardArea.html('Server error during Stripe setup. Please try again.
'); this.$paymentButton.prop('disabled', true); } }); }, /** * Initialize Stripe Elements */ initializeStripeElements: function(clientSecret, $stripeMethodEntry) { if (!this.stripeInstance) { this.stripeInstance = Stripe(easy_invoice_vars.stripe_public_key); } const elements = this.stripeInstance.elements({ clientSecret: clientSecret }); // Clean up previous card element if (this.cardElement) { this.cardElement.destroy(); } this.cardElement = elements.create('card'); const $stripeCardArea = $stripeMethodEntry.find('#stripe-card-area'); const $cardElementDiv = $stripeCardArea.find('#card-element'); const $cardErrorsDiv = $stripeCardArea.find('#card-errors'); if (!$cardElementDiv.length) { // Card element div not found within stripe card area $stripeCardArea.removeClass('hidden') .html('Stripe card input cannot be displayed. Element missing.
'); this.$paymentButton.prop('disabled', true); return false; } $cardElementDiv.empty(); this.cardElement.mount($cardElementDiv.get(0)); $stripeCardArea.removeClass('hidden'); // Listen for card element changes this.cardElement.on('change', (event) => { if (event.error) { $cardErrorsDiv.text(event.error.message); this.$paymentButton.prop('disabled', true); } else { $cardErrorsDiv.text(''); this.$paymentButton.prop('disabled', !event.complete); } }); return true; }, /** * Handle payment button click */ handlePaymentButtonClick: function(e) { e.preventDefault(); this.$paymentButton.prop('disabled', true); if (!this.selectedGateway) { this.showPaymentMessage('Please select a payment method first.', 'error'); this.$paymentButton.prop('disabled', false); return; } // Show loading state this.$paymentButton.html('Processing...'); // Handle Stripe payment if (this.selectedGateway === 'stripe') { this.processStripePayment(); return; } // Handle other payment methods this.processStandardPayment(); }, /** * Process Stripe payment */ processStripePayment: function() { if (!this.stripeInstance || !this.cardElement || !this.stripeClientSecret) { this.showPaymentMessage('Stripe payment setup is incomplete. Please refresh and try again.', 'error'); this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false); return; } this.stripeInstance.confirmCardPayment(this.stripeClientSecret, { payment_method: { card: this.cardElement } }) .then((result) => { if (result.error) { this.showPaymentMessage(result.error.message, 'error'); this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false); } else if (result.paymentIntent && result.paymentIntent.status === 'succeeded') { // Create payment record this.createPaymentRecord(result.paymentIntent.id); } else { this.showPaymentMessage('Payment processing failed. Please try again.', 'error'); this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false); } }) .catch((error) => { // Stripe.confirmCardPayment error this.showPaymentMessage('Payment processing error. Please try again.', 'error'); this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false); }); }, /** * Process standard (non-Stripe) payment */ processStandardPayment: function() { const invoiceId = this.$paymentButton.data('invoice-id'); if (!invoiceId) { this.showPaymentMessage('Invoice ID is missing. Please refresh the page.', 'error'); this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false); return; } $.ajax({ url: easy_invoice_vars.ajax_url, type: 'POST', data: { action: 'easy_invoice_process_payment', invoice_id: invoiceId, payment_method: this.selectedGateway, payment_nonce: easy_invoice_vars.nonce }, success: (response) => { if (response.success) { this.handlePaymentSuccess(response.data); } else { const errorMsg = response.data && response.data.message ? response.data.message : 'Payment processing failed.'; this.showPaymentMessage(errorMsg, 'error'); this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false); } }, error: (xhr) => { // Payment AJAX error this.showPaymentMessage('Server error during payment. Please try again.', 'error'); this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false); } }); }, /** * Create payment record after successful Stripe payment */ createPaymentRecord: function(transactionId) { const invoiceId = this.$paymentButton.data('invoice-id'); $.ajax({ url: easy_invoice_vars.ajax_url, type: 'POST', data: { action: 'easy_invoice_create_payment_record', invoice_id: invoiceId, payment_method: 'stripe', transaction_id: transactionId, payment_nonce: easy_invoice_vars.nonce }, success: (response) => { if (response.success) { this.handlePaymentSuccess(response.data); } else { const errorMsg = response.data && response.data.message ? response.data.message : 'Payment recorded but failed to update records.'; this.showPaymentMessage(errorMsg, 'warning'); this.$paymentButton.html('Payment Recorded').prop('disabled', true); // Reload page after delay even if there was an error updating records setTimeout(() => { window.location.reload(); }, 3000); } }, error: (xhr) => { // Payment record error this.showPaymentMessage('Payment successful but failed to update records. The administrator has been notified.', 'warning'); this.$paymentButton.html('Payment Recorded').prop('disabled', true); // Reload page after delay setTimeout(() => { window.location.reload(); }, 3000); } }); }, /** * Handle successful payment */ handlePaymentSuccess: function(data) { // Update button state this.$paymentButton.html('Payment Successful!').prop('disabled', true); // Show success message this.showPaymentMessage(data.message || 'Payment successful!', 'success'); // Handle redirect if provided if (data.redirect_url) { setTimeout(() => { window.location.href = data.redirect_url; }, 2000); return; } // Otherwise reload the page after delay setTimeout(() => { window.location.reload(); }, 2000); }, /** * Show payment status message */ showPaymentMessage: function(message, type) { const $messageArea = $('#payment-message-area'); if (!$messageArea.length) { // Payment message area not found return; } // Clear any existing timeout if (this.messageClearTimeout) { clearTimeout(this.messageClearTimeout); } // Determine styling based on message type let bgClass, textClass, icon; switch (type) { case 'success': bgClass = 'bg-green-100 border-green-400'; textClass = 'text-green-700'; icon = 'fa-check-circle'; break; case 'warning': bgClass = 'bg-yellow-100 border-yellow-400'; textClass = 'text-yellow-700'; icon = 'fa-exclamation-triangle'; break; case 'error': default: bgClass = 'bg-red-100 border-red-400'; textClass = 'text-red-700'; icon = 'fa-times-circle'; break; } // Build and show message const $messageHtml = $(`${message}