PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.3
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.3
2.4.3 2.4.4 2.4.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 All 60 releases
easy-invoice / assets / js / payment.js

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

496 lines 19.5 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 // Per-invoice access token from the emailed link (?ik=...).
138 // The server authorises the payment on it — see
139 // InvoiceController::canSubmitPaymentForInvoice().
140 access_token: easy_invoice_vars.access_token || ''
141 },
142 success: (response) => {
143 if (response.success && response.data.client_secret) {
144 this.stripeClientSecret = response.data.client_secret;
145
146 // Setup card element HTML
147 $stripeCardArea.html(`
148 <label for="card-element" class="block text-sm font-medium text-gray-700 mb-1">Credit or debit card</label>
149 <div id="card-element" class="mt-1 p-3 border border-gray-300 rounded-md shadow-sm bg-white"></div>
150 <div id="card-errors" role="alert" class="mt-2 text-sm text-red-600"></div>
151 `);
152
153 if (this.initializeStripeElements(this.stripeClientSecret, $selectedEntry)) {
154 this.$paymentButton.text('Pay with Card');
155
156 // Set button state based on card completeness
157 const currentCardState = this.cardElement._empty ? false : this.cardElement._complete;
158 this.$paymentButton.prop('disabled', !currentCardState);
159 } else {
160 this.$paymentButton.prop('disabled', true);
161 }
162 } else {
163 const errorMsg = response.data && response.data.message
164 ? response.data.message
165 : 'Could not initialize Stripe payment.';
166 $stripeCardArea.html(`<p class="text-red-600 p-2">${errorMsg}</p>`);
167 this.$paymentButton.prop('disabled', true);
168 }
169 },
170 error: (xhr) => {
171 // Stripe client_secret AJAX error
172 $stripeCardArea.html('<p class="text-red-600 p-2">Server error during Stripe setup. Please try again.</p>');
173 this.$paymentButton.prop('disabled', true);
174 }
175 });
176 },
177
178 /**
179 * Initialize Stripe Elements
180 */
181 initializeStripeElements: function(clientSecret, $stripeMethodEntry) {
182 if (!this.stripeInstance) {
183 this.stripeInstance = Stripe(easy_invoice_vars.stripe_public_key);
184 }
185
186 const elements = this.stripeInstance.elements({
187 clientSecret: clientSecret
188 });
189
190 // Clean up previous card element
191 if (this.cardElement) {
192 this.cardElement.destroy();
193 }
194
195 this.cardElement = elements.create('card');
196
197 const $stripeCardArea = $stripeMethodEntry.find('#stripe-card-area');
198 const $cardElementDiv = $stripeCardArea.find('#card-element');
199 const $cardErrorsDiv = $stripeCardArea.find('#card-errors');
200
201 if (!$cardElementDiv.length) {
202 // Card element div not found within stripe card area
203 $stripeCardArea.removeClass('hidden')
204 .html('<p class="text-red-600">Stripe card input cannot be displayed. Element missing.</p>');
205 this.$paymentButton.prop('disabled', true);
206 return false;
207 }
208
209 $cardElementDiv.empty();
210 this.cardElement.mount($cardElementDiv.get(0));
211 $stripeCardArea.removeClass('hidden');
212
213 // Listen for card element changes
214 this.cardElement.on('change', (event) => {
215 if (event.error) {
216 $cardErrorsDiv.text(event.error.message);
217 this.$paymentButton.prop('disabled', true);
218 } else {
219 $cardErrorsDiv.text('');
220 this.$paymentButton.prop('disabled', !event.complete);
221 }
222 });
223
224 return true;
225 },
226
227 /**
228 * Handle payment button click
229 */
230 handlePaymentButtonClick: function(e) {
231 e.preventDefault();
232 this.$paymentButton.prop('disabled', true);
233
234 if (!this.selectedGateway) {
235 this.showPaymentMessage('Please select a payment method first.', 'error');
236 this.$paymentButton.prop('disabled', false);
237 return;
238 }
239
240 // Show loading state
241 this.$paymentButton.html('<i class="fas fa-circle-notch fa-spin mr-2"></i>Processing...');
242
243 // Handle Stripe payment
244 if (this.selectedGateway === 'stripe') {
245 this.processStripePayment();
246 return;
247 }
248
249 // Handle other payment methods
250 this.processStandardPayment();
251 },
252
253 /**
254 * Process Stripe payment
255 */
256 processStripePayment: function() {
257 if (!this.stripeInstance || !this.cardElement || !this.stripeClientSecret) {
258 this.showPaymentMessage('Stripe payment setup is incomplete. Please refresh and try again.', 'error');
259 this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false);
260 return;
261 }
262
263 this.stripeInstance.confirmCardPayment(this.stripeClientSecret, {
264 payment_method: {
265 card: this.cardElement
266 }
267 })
268 .then((result) => {
269 if (result.error) {
270 this.showPaymentMessage(result.error.message, 'error');
271 this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false);
272 } else if (result.paymentIntent && result.paymentIntent.status === 'succeeded') {
273 // Create payment record
274 this.createPaymentRecord(result.paymentIntent.id);
275 } else {
276 this.showPaymentMessage('Payment processing failed. Please try again.', 'error');
277 this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false);
278 }
279 })
280 .catch((error) => {
281 // Stripe.confirmCardPayment error
282 this.showPaymentMessage('Payment processing error. Please try again.', 'error');
283 this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false);
284 });
285 },
286
287
288
289 /**
290 * Process standard (non-Stripe) payment
291 */
292 processStandardPayment: function() {
293 const invoiceId = this.$paymentButton.data('invoice-id');
294 if (!invoiceId) {
295 this.showPaymentMessage('Invoice ID is missing. Please refresh the page.', 'error');
296 this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false);
297 return;
298 }
299
300 $.ajax({
301 url: easy_invoice_vars.ajax_url,
302 type: 'POST',
303 data: {
304 action: 'easy_invoice_process_payment',
305 invoice_id: invoiceId,
306 payment_method: this.selectedGateway,
307 payment_nonce: easy_invoice_vars.nonce,
308 access_token: easy_invoice_vars.access_token || ''
309 },
310 success: (response) => {
311 if (response.success) {
312 this.handlePaymentSuccess(response.data);
313 } else {
314 const errorMsg = response.data && response.data.message
315 ? response.data.message
316 : 'Payment processing failed.';
317 this.showPaymentMessage(errorMsg, 'error');
318 this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false);
319 }
320 },
321 error: (xhr) => {
322 // Payment AJAX error
323 this.showPaymentMessage('Server error during payment. Please try again.', 'error');
324 this.$paymentButton.html(this.originalPayButtonText).prop('disabled', false);
325 }
326 });
327 },
328
329 /**
330 * Create payment record after successful Stripe payment
331 */
332 createPaymentRecord: function(transactionId) {
333 const invoiceId = this.$paymentButton.data('invoice-id');
334
335 $.ajax({
336 url: easy_invoice_vars.ajax_url,
337 type: 'POST',
338 data: {
339 action: 'easy_invoice_create_payment_record',
340 invoice_id: invoiceId,
341 payment_method: 'stripe',
342 transaction_id: transactionId,
343 payment_nonce: easy_invoice_vars.nonce
344 },
345 success: (response) => {
346 if (response.success) {
347 this.handlePaymentSuccess(response.data);
348 } else {
349 const errorMsg = response.data && response.data.message
350 ? response.data.message
351 : 'Payment recorded but failed to update records.';
352 this.showPaymentMessage(errorMsg, 'warning');
353 this.$paymentButton.html('Payment Recorded').prop('disabled', true);
354
355 // Reload page after delay even if there was an error updating records
356 setTimeout(() => {
357 window.location.reload();
358 }, 3000);
359 }
360 },
361 error: (xhr) => {
362 // Payment record error
363 this.showPaymentMessage('Payment successful but failed to update records. The administrator has been notified.', 'warning');
364 this.$paymentButton.html('Payment Recorded').prop('disabled', true);
365
366 // Reload page after delay
367 setTimeout(() => {
368 window.location.reload();
369 }, 3000);
370 }
371 });
372 },
373
374 /**
375 * Handle successful payment
376 */
377 handlePaymentSuccess: function(data) {
378 // Update button state
379 this.$paymentButton.html('<i class="fas fa-check-circle mr-2"></i>Payment Successful!').prop('disabled', true);
380
381 // Show success message
382 this.showPaymentMessage(data.message || 'Payment successful!', 'success');
383
384 // Handle redirect if provided
385 if (data.redirect_url) {
386 setTimeout(() => {
387 window.location.href = data.redirect_url;
388 }, 2000);
389 return;
390 }
391
392 // Otherwise reload the page after delay
393 setTimeout(() => {
394 window.location.reload();
395 }, 2000);
396 },
397
398 /**
399 * Show payment status message
400 */
401 showPaymentMessage: function(message, type) {
402 const $messageArea = $('#payment-message-area');
403 if (!$messageArea.length) {
404 // Payment message area not found
405 return;
406 }
407
408 // Clear any existing timeout
409 if (this.messageClearTimeout) {
410 clearTimeout(this.messageClearTimeout);
411 }
412
413 // Determine styling based on message type
414 let bgClass, textClass, icon;
415
416 switch (type) {
417 case 'success':
418 bgClass = 'bg-green-100 border-green-400';
419 textClass = 'text-green-700';
420 icon = 'fa-check-circle';
421 break;
422 case 'warning':
423 bgClass = 'bg-yellow-100 border-yellow-400';
424 textClass = 'text-yellow-700';
425 icon = 'fa-exclamation-triangle';
426 break;
427 case 'error':
428 default:
429 bgClass = 'bg-red-100 border-red-400';
430 textClass = 'text-red-700';
431 icon = 'fa-times-circle';
432 break;
433 }
434
435 // Build and show message
436 const $messageHtml = $(`
437 <div class="rounded-md border px-4 py-3 ${bgClass} mb-4 animate-fade-in">
438 <div class="flex items-center">
439 <div class="flex-shrink-0">
440 <i class="fas ${icon} ${textClass}"></i>
441 </div>
442 <div class="ml-3">
443 <p class="text-sm ${textClass}">${message}</p>
444 </div>
445 </div>
446 </div>
447 `);
448
449 $messageArea.html($messageHtml);
450
451 // Auto-hide messages except for success ones
452 if (type !== 'success') {
453 this.messageClearTimeout = setTimeout(() => {
454 $messageHtml.fadeOut(300, function() {
455 $(this).remove();
456 });
457 }, 6000);
458 }
459 },
460
461 /**
462 * Handle panel open button click
463 */
464 handleOpenPanel: function(e) {
465 e.preventDefault();
466 this.$paymentPanel.addClass('is-open');
467 $('body').addClass('body-panel-active');
468 this.$closePaymentPanelButton.focus();
469 },
470
471 /**
472 * Handle panel close button click
473 */
474 handleClosePanel: function(e) {
475 e.preventDefault();
476 this.$paymentPanel.removeClass('is-open');
477 $('body').removeClass('body-panel-active');
478 this.$openPaymentPanelButton.focus();
479 },
480
481 /**
482 * Handle keyboard events
483 */
484 handleKeyDown: function(event) {
485 if (event.key === "Escape" && this.$paymentPanel.hasClass('is-open')) {
486 this.handleClosePanel(event);
487 }
488 }
489 };
490
491 // Initialize when document is ready
492 $(document).ready(function() {
493 EasyInvoicePayment.init();
494 });
495
496 })(jQuery);