PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.2
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / assets / js / payment.js

payment.js in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.3.2, at assets/js/payment.js

491 lines 19.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Easy Invoice Payment Handler
3 *
4 * @package EasyInvoice
5 * @since 1.0.0
6 */
7 (function($) {
8 'use strict';
9
10 // Module pattern for better encapsulation
11 const EasyInvoicePayment = {
12 // Properties
13 initialized: false,
14 selectedGateway: null,
15 stripeInstance: null,
16 cardElement: null,
17 stripeClientSecret: null,
18 $paymentButton: null,
19 originalPayButtonText: '',
20 messageClearTimeout: null,
21 $paymentPanel: null,
22 $mainContentArea: null,
23 $openPaymentPanelButton: null,
24 $closePaymentPanelButton: null,
25
26 /**
27 * Initialize the payment handler
28 */
29 init: function() {
30 // Prevent multiple initializations
31 if (window.easyInvoicePaymentInitialized || this.initialized) {
32 return;
33 }
34 this.initialized = true;
35 window.easyInvoicePaymentInitialized = true;
36
37 // Cache DOM elements
38 this.$paymentButton = $('.payment-button');
39 this.originalPayButtonText = this.$paymentButton.text() || 'Pay Now';
40 this.$paymentPanel = $('#payment-slideout-panel');
41 this.$mainContentArea = $('.easy-invoice-main-content-area');
42 this.$openPaymentPanelButton = $('#open-payment-panel-button');
43 this.$closePaymentPanelButton = $('#close-payment-panel-button');
44
45 // Setup event handlers
46 this.setupEventListeners();
47
48 // Show default payment method if pre-selected
49 const $defaultMethod = $('input[name="payment_method_radio"]:checked');
50 if ($defaultMethod.length) {
51 $defaultMethod.trigger('change');
52 }
53 },
54
55 /**
56 * Setup all event listeners
57 */
58 setupEventListeners: function() {
59 // Payment method selection
60 $('input[name="payment_method_radio"]').on('change', this.handlePaymentMethodChange.bind(this));
61
62 // Payment button
63 this.$paymentButton.on('click', this.handlePaymentButtonClick.bind(this));
64
65 // Panel open/close
66 this.$openPaymentPanelButton.on('click', this.handleOpenPanel.bind(this));
67 this.$closePaymentPanelButton.on('click', this.handleClosePanel.bind(this));
68
69 // Close panel with Escape key
70 $(document).on('keydown', this.handleKeyDown.bind(this));
71 },
72
73 /**
74 * Handle payment method selection change
75 */
76 handlePaymentMethodChange: function(e) {
77 this.selectedGateway = $(e.currentTarget).val();
78 const $selectedEntry = $(e.currentTarget).closest('.payment-method-entry');
79
80 // Update UI
81 $('.payment-method-entry').removeClass('selected-gateway border-indigo-600 shadow-lg')
82 .addClass('border-gray-300 shadow-sm');
83 $selectedEntry.addClass('selected-gateway border-indigo-600 shadow-lg')
84 .removeClass('border-gray-300 shadow-sm');
85
86 // Hide all payment form areas
87 $('.easy-invoice-stripe-card-area').addClass('hidden');
88
89 // Clear card errors if any
90 if (this.cardElement) {
91 const $cardErrorsDiv = $('#card-errors');
92 if ($cardErrorsDiv.length) {
93 $cardErrorsDiv.text('');
94 }
95 }
96
97 // Reset payment button
98 this.$paymentButton.text(this.originalPayButtonText);
99
100 // Handle Stripe gateway
101 if (this.selectedGateway === 'stripe') {
102 this.initializeStripePayment($selectedEntry);
103 } else {
104 // For other gateways, enable payment button immediately
105 this.$paymentButton.prop('disabled', false);
106 }
107 },
108
109 /**
110 * Initialize Stripe payment elements
111 */
112 initializeStripePayment: function($selectedEntry) {
113 const $stripeCardArea = $selectedEntry.find('#stripe-card-area');
114 if (!$stripeCardArea.length) {
115 // Stripe card area not found
116 this.showPaymentMessage('Stripe UI setup error. Please try refreshing.', 'error');
117 return;
118 }
119
120 $stripeCardArea.removeClass('hidden').html('<p class="text-sm text-gray-600 p-2">Loading card details...</p>');
121
122 const invoiceId = this.$paymentButton.data('invoice-id');
123 if (!invoiceId) {
124 $stripeCardArea.html('<p class="text-red-600 p-2">Error: Invoice ID missing.</p>');
125 return;
126 }
127
128 // Request client secret from server
129 $.ajax({
130 url: easy_invoice_vars.ajax_url,
131 type: 'POST',
132 data: {
133 action: 'easy_invoice_process_payment',
134 invoice_id: invoiceId,
135 payment_method: 'stripe',
136 payment_nonce: easy_invoice_vars.nonce
137 },
138 success: (response) => {
139 if (response.success && response.data.client_secret) {
140 this.stripeClientSecret = response.data.client_secret;
141
142 // Setup card element HTML
143 $stripeCardArea.html(`
144 <label for="card-element" class="block text-sm font-medium text-gray-700 mb-1">Credit or debit card</label>
145 <div id="card-element" class="mt-1 p-3 border border-gray-300 rounded-md shadow-sm bg-white"></div>
146 <div id="card-errors" role="alert" class="mt-2 text-sm text-red-600"></div>
147 `);
148
149 if (this.initializeStripeElements(this.stripeClientSecret, $selectedEntry)) {
150 this.$paymentButton.text('Pay with Card');
151
152 // Set button state based on card completeness
153 const currentCardState = this.cardElement._empty ? false : this.cardElement._complete;
154 this.$paymentButton.prop('disabled', !currentCardState);
155 } else {
156 this.$paymentButton.prop('disabled', true);
157 }
158 } else {
159 const errorMsg = response.data && response.data.message
160 ? response.data.message
161 : 'Could not initialize Stripe payment.';
162 $stripeCardArea.html(`<p class="text-red-600 p-2">${errorMsg}</p>`);
163 this.$paymentButton.prop('disabled', true);
164 }
165 },
166 error: (xhr) => {
167 // Stripe client_secret AJAX error
168 $stripeCardArea.html('<p class="text-red-600 p-2">Server error during Stripe setup. Please try again.</p>');
169 this.$paymentButton.prop('disabled', true);
170 }
171 });
172 },
173
174 /**
175 * Initialize Stripe Elements
176 */
177 initializeStripeElements: function(clientSecret, $stripeMethodEntry) {
178 if (!this.stripeInstance) {
179 this.stripeInstance = Stripe(easy_invoice_vars.stripe_public_key);
180 }
181
182 const elements = this.stripeInstance.elements({
183 clientSecret: clientSecret
184 });
185
186 // Clean up previous card element
187 if (this.cardElement) {
188 this.cardElement.destroy();
189 }
190
191 this.cardElement = elements.create('card');
192
193 const $stripeCardArea = $stripeMethodEntry.find('#stripe-card-area');
194 const $cardElementDiv = $stripeCardArea.find('#card-element');
195 const $cardErrorsDiv = $stripeCardArea.find('#card-errors');
196
197 if (!$cardElementDiv.length) {
198 // Card element div not found within stripe card area
199 $stripeCardArea.removeClass('hidden')
200 .html('<p class="text-red-600">Stripe card input cannot be displayed. Element missing.</p>');
201 this.$paymentButton.prop('disabled', true);
202 return false;
203 }
204
205 $cardElementDiv.empty();
206 this.cardElement.mount($cardElementDiv.get(0));
207 $stripeCardArea.removeClass('hidden');
208
209 // Listen for card element changes
210 this.cardElement.on('change', (event) => {
211 if (event.error) {
212 $cardErrorsDiv.text(event.error.message);
213 this.$paymentButton.prop('disabled', true);
214 } else {
215 $cardErrorsDiv.text('');
216 this.$paymentButton.prop('disabled', !event.complete);
217 }
218 });
219
220 return true;
221 },
222
223 /**
224 * Handle payment button click
225 */
226 handlePaymentButtonClick: function(e) {
227 e.preventDefault();
228 this.$paymentButton.prop('disabled', true);
229
230 if (!this.selectedGateway) {
231 this.showPaymentMessage('Please select a payment method first.', 'error');
232 this.$paymentButton.prop('disabled', false);
233 return;
234 }
235
236 // Show loading state
237 this.$paymentButton.html('<i class="fas fa-circle-notch fa-spin mr-2"></i>Processing...');
238
239 // Handle Stripe payment
240 if (this.selectedGateway === 'stripe') {
241 this.processStripePayment();
242 return;
243 }
244
245 // Handle other payment methods
246 this.processStandardPayment();
247 },
248
249 /**
250 * Process Stripe payment
251 */
252 processStripePayment: function() {
253 if (!this.stripeInstance || !this.cardElement || !this.stripeClientSecret) {
254 this.showPaymentMessage('Stripe payment setup is incomplete. Please refresh and try again.', 'error');
255 this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false);
256 return;
257 }
258
259 this.stripeInstance.confirmCardPayment(this.stripeClientSecret, {
260 payment_method: {
261 card: this.cardElement
262 }
263 })
264 .then((result) => {
265 if (result.error) {
266 this.showPaymentMessage(result.error.message, 'error');
267 this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false);
268 } else if (result.paymentIntent && result.paymentIntent.status === 'succeeded') {
269 // Create payment record
270 this.createPaymentRecord(result.paymentIntent.id);
271 } else {
272 this.showPaymentMessage('Payment processing failed. Please try again.', 'error');
273 this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false);
274 }
275 })
276 .catch((error) => {
277 // Stripe.confirmCardPayment error
278 this.showPaymentMessage('Payment processing error. Please try again.', 'error');
279 this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false);
280 });
281 },
282
283
284
285 /**
286 * Process standard (non-Stripe) payment
287 */
288 processStandardPayment: function() {
289 const invoiceId = this.$paymentButton.data('invoice-id');
290 if (!invoiceId) {
291 this.showPaymentMessage('Invoice ID is missing. Please refresh the page.', 'error');
292 this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false);
293 return;
294 }
295
296 $.ajax({
297 url: easy_invoice_vars.ajax_url,
298 type: 'POST',
299 data: {
300 action: 'easy_invoice_process_payment',
301 invoice_id: invoiceId,
302 payment_method: this.selectedGateway,
303 payment_nonce: easy_invoice_vars.nonce
304 },
305 success: (response) => {
306 if (response.success) {
307 this.handlePaymentSuccess(response.data);
308 } else {
309 const errorMsg = response.data && response.data.message
310 ? response.data.message
311 : 'Payment processing failed.';
312 this.showPaymentMessage(errorMsg, 'error');
313 this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false);
314 }
315 },
316 error: (xhr) => {
317 // Payment AJAX error
318 this.showPaymentMessage('Server error during payment. Please try again.', 'error');
319 this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false);
320 }
321 });
322 },
323
324 /**
325 * Create payment record after successful Stripe payment
326 */
327 createPaymentRecord: function(transactionId) {
328 const invoiceId = this.$paymentButton.data('invoice-id');
329
330 $.ajax({
331 url: easy_invoice_vars.ajax_url,
332 type: 'POST',
333 data: {
334 action: 'easy_invoice_create_payment_record',
335 invoice_id: invoiceId,
336 payment_method: 'stripe',
337 transaction_id: transactionId,
338 payment_nonce: easy_invoice_vars.nonce
339 },
340 success: (response) => {
341 if (response.success) {
342 this.handlePaymentSuccess(response.data);
343 } else {
344 const errorMsg = response.data && response.data.message
345 ? response.data.message
346 : 'Payment recorded but failed to update records.';
347 this.showPaymentMessage(errorMsg, 'warning');
348 this.$paymentButton.html('Payment Recorded').prop('disabled', true);
349
350 // Reload page after delay even if there was an error updating records
351 setTimeout(() => {
352 window.location.reload();
353 }, 3000);
354 }
355 },
356 error: (xhr) => {
357 // Payment record error
358 this.showPaymentMessage('Payment successful but failed to update records. The administrator has been notified.', 'warning');
359 this.$paymentButton.html('Payment Recorded').prop('disabled', true);
360
361 // Reload page after delay
362 setTimeout(() => {
363 window.location.reload();
364 }, 3000);
365 }
366 });
367 },
368
369 /**
370 * Handle successful payment
371 */
372 handlePaymentSuccess: function(data) {
373 // Update button state
374 this.$paymentButton.html('<i class="fas fa-check-circle mr-2"></i>Payment Successful!').prop('disabled', true);
375
376 // Show success message
377 this.showPaymentMessage(data.message || 'Payment successful!', 'success');
378
379 // Handle redirect if provided
380 if (data.redirect_url) {
381 setTimeout(() => {
382 window.location.href = data.redirect_url;
383 }, 2000);
384 return;
385 }
386
387 // Otherwise reload the page after delay
388 setTimeout(() => {
389 window.location.reload();
390 }, 2000);
391 },
392
393 /**
394 * Show payment status message
395 */
396 showPaymentMessage: function(message, type) {
397 const $messageArea = $('#payment-message-area');
398 if (!$messageArea.length) {
399 // Payment message area not found
400 return;
401 }
402
403 // Clear any existing timeout
404 if (this.messageClearTimeout) {
405 clearTimeout(this.messageClearTimeout);
406 }
407
408 // Determine styling based on message type
409 let bgClass, textClass, icon;
410
411 switch (type) {
412 case 'success':
413 bgClass = 'bg-green-100 border-green-400';
414 textClass = 'text-green-700';
415 icon = 'fa-check-circle';
416 break;
417 case 'warning':
418 bgClass = 'bg-yellow-100 border-yellow-400';
419 textClass = 'text-yellow-700';
420 icon = 'fa-exclamation-triangle';
421 break;
422 case 'error':
423 default:
424 bgClass = 'bg-red-100 border-red-400';
425 textClass = 'text-red-700';
426 icon = 'fa-times-circle';
427 break;
428 }
429
430 // Build and show message
431 const $messageHtml = $(`
432 <div class="rounded-md border px-4 py-3 ${bgClass} mb-4 animate-fade-in">
433 <div class="flex items-center">
434 <div class="flex-shrink-0">
435 <i class="fas ${icon} ${textClass}"></i>
436 </div>
437 <div class="ml-3">
438 <p class="text-sm ${textClass}">${message}</p>
439 </div>
440 </div>
441 </div>
442 `);
443
444 $messageArea.html($messageHtml);
445
446 // Auto-hide messages except for success ones
447 if (type !== 'success') {
448 this.messageClearTimeout = setTimeout(() => {
449 $messageHtml.fadeOut(300, function() {
450 $(this).remove();
451 });
452 }, 6000);
453 }
454 },
455
456 /**
457 * Handle panel open button click
458 */
459 handleOpenPanel: function(e) {
460 e.preventDefault();
461 this.$paymentPanel.addClass('is-open');
462 $('body').addClass('body-panel-active');
463 this.$closePaymentPanelButton.focus();
464 },
465
466 /**
467 * Handle panel close button click
468 */
469 handleClosePanel: function(e) {
470 e.preventDefault();
471 this.$paymentPanel.removeClass('is-open');
472 $('body').removeClass('body-panel-active');
473 this.$openPaymentPanelButton.focus();
474 },
475
476 /**
477 * Handle keyboard events
478 */
479 handleKeyDown: function(event) {
480 if (event.key === "Escape" && this.$paymentPanel.hasClass('is-open')) {
481 this.handleClosePanel(event);
482 }
483 }
484 };
485
486 // Initialize when document is ready
487 $(document).ready(function() {
488 EasyInvoicePayment.init();
489 });
490
491 })(jQuery);