PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.1
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 / templates / payment-section.php

payment-section.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.1, at templates/payment-section.php

1,033 lines 30.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Payment Section Template
4 *
5 * Displays payment options and processing UI
6 *
7 * @package EasyInvoice
8 * @since 1.0.0
9 */
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14 // Get invoice data
15 $invoice_id = get_the_ID();
16 // The access token the AJAX calls below present back to the payment endpoints:
17 // the ?ik= from an emailed link, or whatever another proof of access (a signed
18 // secure link) supplies through the filter.
19 /** This filter is documented in includes/Controllers/InvoiceController.php */
20 $ei_ps_access_token = (string) apply_filters('easy_invoice_presented_access_token', isset($_GET['ik']) ? sanitize_text_field(wp_unslash($_GET['ik'])) : '', 'invoice'); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
21 $invoice = new \EasyInvoice\Models\Invoice(get_post($invoice_id));
22 // What is owed now: total less payments received and credit notes.
23 $total_amount = \EasyInvoice\Services\InvoiceBalance::due($invoice);
24 $currency_code = $invoice->getCurrencyCode() ?: 'USD';
25
26 // If currency is "global", use the global setting
27 if ($currency_code === 'global') {
28 $currency_code = get_option('easy_invoice_currency_code', 'USD');
29 }
30
31 $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol($currency_code);
32
33 // Get available payment gateways
34 $payment_controller = new \EasyInvoice\Controllers\PaymentController();
35 $available_gateways = $payment_controller->getAvailableGateways($invoice_id);
36
37 // Get nonce for AJAX security
38 $payment_nonce = wp_create_nonce('easy_invoice_payment');
39
40
41 ?>
42 <?php
43 if (!function_exists('easy_invoice_payment_icon')) {
44 /**
45 * An inline icon for a payment method. The public page loads no icon
46 * font, so Font Awesome class names rendered as empty boxes; a gateway
47 * may still hand over an image URL.
48 *
49 * @param string $gateway_id Gateway id.
50 * @param string $icon Icon the gateway declared (class name or URL).
51 * @return string HTML.
52 */
53 function easy_invoice_payment_icon(string $gateway_id, string $icon = ''): string {
54 if ('' !== $icon && preg_match('#^(https?:)?//|^/#', $icon)) {
55 return '<img src="' . esc_url($icon) . '" alt="" style="width:22px;height:22px;object-fit:contain;">';
56 }
57 $a = 'width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"';
58 $paths = [
59 'bank' => '<path d="M3 10h18M5 10v9M9 10v9M15 10v9M19 10v9M2 19h20M12 3l9 6H3l9-6z"/>',
60 'cheque' => '<rect x="3" y="6" width="18" height="12" rx="2"/><path d="M7 10h6M7 14h3M15 14h2"/>',
61 'cash' => '<rect x="2" y="6" width="20" height="12" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M6 9h.01M18 15h.01"/>',
62 'card' => '<rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20M6 15h4"/>',
63 'wallet' => '<path d="M3 7a2 2 0 0 1 2-2h14v4"/><rect x="3" y="7" width="18" height="12" rx="2"/><path d="M16 13h.01"/>',
64 ];
65 $id = strtolower($gateway_id);
66 $key = 'card';
67 if (false !== strpos($id, 'bank') || false !== strpos($id, 'transfer') || false !== strpos($id, 'wire')) { $key = 'bank'; }
68 elseif (false !== strpos($id, 'cheque') || false !== strpos($id, 'check')) { $key = 'cheque'; }
69 elseif (false !== strpos($id, 'cash')) { $key = 'cash'; }
70 elseif (false !== strpos($id, 'paypal') || false !== strpos($id, 'paystack') || false !== strpos($id, 'mollie')) { $key = 'wallet'; }
71 return '<svg ' . $a . '>' . $paths[$key] . '</svg>';
72 }
73 }
74 ?>
75
76
77 <div class="easy-invoice-payment-sidebar">
78 <div class="payment-header">
79 <div class="header-content">
80 <h2><?php esc_html_e('Payment', 'easy-invoice'); ?></h2>
81 <div class="invoice-summary">
82 <span class="invoice-number"><?php echo esc_html($invoice->getNumber()); ?></span>
83 <span class="invoice-date"><?php echo esc_html(date_i18n('M j, Y', strtotime($invoice->getIssueDate()))); ?></span>
84 </div>
85 </div>
86
87 <div class="amount-display">
88 <div class="amount-label"><?php echo esc_html((float) $invoice->getTotal() - $total_amount > 0.005 ? __('Amount Due', 'easy-invoice') : __('Total Amount', 'easy-invoice')); ?></div>
89 <div class="amount-value">
90 <?php echo esc_html(easy_invoice_format_money($total_amount, $invoice)); ?>
91 </div>
92 </div>
93 </div>
94
95 <?php if (empty($available_gateways)): ?>
96 <div class="no-payment-methods">
97 <div class="empty-state">
98 <div class="empty-icon">
99 <?php echo easy_invoice_payment_icon('card'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
100 </div>
101 <h3><?php esc_html_e('No Payment Methods', 'easy-invoice'); ?></h3>
102 <p><?php esc_html_e('Payment methods are not configured for this invoice.', 'easy-invoice'); ?></p>
103 </div>
104 </div>
105 <?php else: ?>
106 <form id="easy-invoice-payment-form" class="payment-form">
107 <input type="hidden" name="invoice_id" value="<?php echo esc_attr($invoice_id); ?>">
108 <input type="hidden" name="payment_nonce" value="<?php echo esc_attr($payment_nonce); ?>">
109 <input type="hidden" name="payment_amount" id="payment_amount" value="<?php echo esc_attr($total_amount); ?>">
110 <input type="hidden" name="payment_note" id="payment_note" value="">
111 <input type="hidden" name="action" value="easy_invoice_process_payment">
112 <input type="hidden" name="is_partial_payment" id="is_partial_payment" value="0">
113 <input type="hidden" name="payment_method" id="payment_method" value="">
114
115 <?php
116 // Add partial payments form before payment gateways (only if enabled)
117 do_action('easy_invoice_payment_gateways_before', $invoice);
118
119 ?>
120
121 <div class="payment-methods">
122 <label class="section-label"><?php esc_html_e('Payment Method', 'easy-invoice'); ?></label>
123
124 <div class="method-options">
125 <?php foreach ($available_gateways as $gateway): ?>
126 <div class="method-option" data-gateway="<?php echo esc_attr($gateway['id']); ?>">
127 <input type="radio"
128 name="payment_method_radio"
129 id="payment_method_<?php echo esc_attr($gateway['id']); ?>"
130 value="<?php echo esc_attr($gateway['id']); ?>"
131 class="method-radio">
132
133 <label for="payment_method_<?php echo esc_attr($gateway['id']); ?>" class="method-label">
134 <div class="method-content">
135 <div class="method-icon">
136 <?php echo easy_invoice_payment_icon((string) $gateway['id'], (string) ($gateway['icon'] ?? '')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- inline SVG / escaped img. ?>
137 </div>
138
139 <div class="method-details">
140 <div class="method-name"><?php echo esc_html($gateway['title']); ?></div>
141 <?php if (!empty($gateway['description'])): ?>
142 <div class="method-description"><?php echo esc_html($gateway['description']); ?></div>
143 <?php endif; ?>
144 </div>
145
146 <div class="method-check">
147 <div class="checkmark"></div>
148 </div>
149 </div>
150 </label>
151
152 <div class="gateway-content" id="gateway-content-<?php echo esc_attr($gateway['id']); ?>" style="display: none;"></div>
153 </div>
154 <?php endforeach; ?>
155 </div>
156 </div>
157
158 <div class="message-area" id="payment-message-area"></div>
159
160 <div class="submit-section">
161 <button type="submit" id="pay-now-button" class="submit-button" disabled>
162 <span class="button-text"><?php esc_html_e('Pay', 'easy-invoice'); ?> <?php echo esc_html(easy_invoice_format_money($total_amount, $invoice)); ?></span>
163 <div class="button-loader hidden">
164 <div class="loader"></div>
165 <span><?php esc_html_e('Processing...', 'easy-invoice'); ?></span>
166 </div>
167 </button>
168
169 <div class="security-note">
170 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="vertical-align:-2px;margin-right:4px;"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
171 <span><?php esc_html_e('Your payment is secure and encrypted', 'easy-invoice'); ?></span>
172 </div>
173 </div>
174 </form>
175 <?php endif; ?>
176 </div>
177
178 <style>
179 /* Clean SaaS Payment Sidebar */
180 .easy-invoice-payment-sidebar {
181 background: #ffffff;
182 height: 100vh;
183 display: flex;
184 flex-direction: column;
185 border-left: 1px solid #e5e7eb;
186 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
187 box-shadow: -4px 0 12px rgba(0, 0, 0, 0.05);
188 }
189
190 /* Header */
191 .payment-header {
192 padding: 32px 24px 24px;
193 border-bottom: 1px solid #f3f4f6;
194 background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
195 }
196
197 .header-content h2 {
198 margin: 0 0 8px 0;
199 font-size: 24px;
200 font-weight: 600;
201 color: #111827;
202 line-height: 1.2;
203 }
204
205 .invoice-summary {
206 display: flex;
207 align-items: center;
208 gap: 12px;
209 font-size: 14px;
210 color: #6b7280;
211 }
212
213 .invoice-number {
214 font-weight: 500;
215 color: #374151;
216 }
217
218 .invoice-date {
219 color: #9ca3af;
220 }
221
222 .amount-display {
223 margin-top: 20px;
224 padding: 20px;
225 background: #ffffff;
226 border: 1px solid #e5e7eb;
227 border-radius: 12px;
228 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
229 }
230
231 .amount-label {
232 font-size: 12px;
233 font-weight: 500;
234 color: #6b7280;
235 text-transform: uppercase;
236 letter-spacing: 0.5px;
237 margin-bottom: 4px;
238 }
239
240 .amount-value {
241 font-size: 18px;
242 font-weight: 700;
243 color: #111827;
244 line-height: 1;
245 }
246
247 /* No Payment Methods */
248 .no-payment-methods {
249 flex: 1;
250 display: flex;
251 align-items: center;
252 justify-content: center;
253 padding: 40px 24px;
254 }
255
256 .empty-state {
257 text-align: center;
258 max-width: 280px;
259 }
260
261 .empty-icon {
262 width: 48px;
263 height: 48px;
264 background: #f3f4f6;
265 border-radius: 12px;
266 display: flex;
267 align-items: center;
268 justify-content: center;
269 margin: 0 auto 16px;
270 color: #9ca3af;
271 font-size: 20px;
272 }
273
274 .empty-state h3 {
275 margin: 0 0 8px 0;
276 font-size: 16px;
277 font-weight: 600;
278 color: #374151;
279 }
280
281 .empty-state p {
282 margin: 0;
283 font-size: 14px;
284 color: #6b7280;
285 line-height: 1.5;
286 }
287
288 /* Payment Form */
289 .payment-form {
290 flex: 1;
291 display: flex;
292 flex-direction: column;
293 padding: 24px;
294 overflow-y: auto;
295 }
296
297 /* Payment Methods */
298 .payment-methods {
299 margin-bottom: 24px;
300 }
301
302 .section-label {
303 display: block;
304 font-size: 14px;
305 font-weight: 600;
306 color: #374151;
307 margin-bottom: 16px;
308 }
309
310 .method-options {
311 display: flex;
312 flex-direction: column;
313 gap: 12px;
314 }
315
316 /* Gateway Content */
317 .gateway-content {
318 padding: 20px;
319 background: #ffffff;
320 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
321 border-top: 2px solid #10b981;
322 }
323
324 .gateway-content h4 {
325 margin: 0 0 16px 0;
326 font-size: 16px;
327 font-weight: 600;
328 color: #111827;
329 line-height: 1.4;
330 }
331
332 .gateway-content p {
333 margin: 0 0 12px 0;
334 font-size: 14px;
335 line-height: 1.5;
336 color: #374151;
337 }
338
339 .gateway-content .bank-details,
340 .gateway-content .payment-reference {
341 margin: 16px 0;
342 padding: 16px;
343 background: #f8fafc;
344 border-radius: 8px;
345 border: 1px solid #e5e7eb;
346 }
347
348 .gateway-content .payment-reference {
349 background: #f0f7ff;
350 border-left: 4px solid #0073aa;
351 }
352
353 .gateway-content .reference-note {
354 font-style: italic;
355 color: #6b7280;
356 margin-top: 8px;
357 font-size: 13px;
358 }
359
360 .gateway-content .payment-proof-form {
361 margin-top: 24px;
362 padding-top: 20px;
363 border-top: 1px solid #e5e7eb;
364 }
365
366
367
368 /* Gateway-specific instruction styles */
369 .gateway-content .bank-transfer-instructions,
370 .gateway-content .cheque-payment-instructions,
371 .gateway-content .cash-payment-instructions {
372 margin: 0;
373 padding: 0;
374 background: transparent;
375 border: none;
376 border-radius: 0;
377 border-left: none;
378 }
379
380 .gateway-content .bank-details-table {
381 width: 100%;
382 border-collapse: collapse;
383 margin-bottom: 16px;
384 }
385
386 .gateway-content .bank-details-table th,
387 .gateway-content .bank-details-table td {
388 padding: 8px 12px;
389 border-bottom: 1px solid #e5e7eb;
390 text-align: left;
391 }
392
393 .gateway-content .bank-details-table th {
394 width: 40%;
395 font-weight: 600;
396 color: #374151;
397 }
398
399 .gateway-content .cheque-details,
400 .gateway-content .cash-details {
401 margin-bottom: 16px;
402 }
403
404 .gateway-content .payable-to,
405 .gateway-content .invoice-number {
406 font-size: 16px;
407 margin: 8px 0 16px;
408 color: #0073aa;
409 font-weight: 600;
410 }
411
412 .gateway-content .mailing-address {
413 padding: 12px;
414 background: #f0f7ff;
415 border-left: 3px solid #0073aa;
416 margin-top: 8px;
417 border-radius: 6px;
418 }
419
420 .gateway-content .cheque-notification-form {
421 margin-top: 20px;
422 border-top: 1px solid #e5e7eb;
423 padding-top: 16px;
424 }
425
426 .gateway-content .submit-notification-btn {
427 background: #0073aa;
428 color: white;
429 padding: 10px 20px;
430 border: none;
431 border-radius: 6px;
432 cursor: pointer;
433 font-size: 14px;
434 font-weight: 500;
435 transition: background-color 0.2s ease;
436 }
437
438 .gateway-content .submit-notification-btn:hover {
439 background: #005d8c;
440 }
441
442 .gateway-content .submit-notification-btn:disabled {
443 background: #9ca3af;
444 cursor: not-allowed;
445 }
446
447
448
449 .method-option {
450 border: 2px solid #e5e7eb;
451 border-radius: 12px;
452 transition: all 0.2s ease;
453 background: #ffffff;
454 overflow: hidden;
455 }
456
457 .method-option:hover {
458 border-color: #d1d5db;
459 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
460 transform: translateY(-1px);
461 }
462
463 .method-option.selected {
464 border-color: #10b981;
465 box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
466 background: #f0fdf4;
467 }
468
469 .method-radio {
470 display: none;
471 }
472
473 .method-label {
474 display: block;
475 padding: 20px;
476 cursor: pointer;
477 margin: 0;
478 }
479
480 .method-content {
481 display: flex;
482 align-items: center;
483 gap: 16px;
484 }
485
486 .method-icon {
487 width: 40px;
488 height: 40px;
489 background: #f9fafb;
490 border-radius: 10px;
491 display: flex;
492 align-items: center;
493 justify-content: center;
494 color: #6b7280;
495 font-size: 18px;
496 flex-shrink: 0;
497 transition: all 0.2s ease;
498 }
499
500 .method-option.selected .method-icon {
501 background: #10b981;
502 color: #ffffff;
503 }
504
505 .method-details {
506 flex: 1;
507 min-width: 0;
508 }
509
510 .method-name {
511 font-weight: 600;
512 font-size: 15px;
513 color: #111827;
514 margin-bottom: 4px;
515 }
516
517 .method-description {
518 font-size: 13px;
519 color: #6b7280;
520 line-height: 1.4;
521 }
522
523 .method-check {
524 flex-shrink: 0;
525 }
526
527 .checkmark {
528 width: 24px;
529 height: 24px;
530 border: 2px solid #d1d5db;
531 border-radius: 50%;
532 position: relative;
533 transition: all 0.2s ease;
534 }
535
536 .checkmark::after {
537 content: '';
538 position: absolute;
539 top: 50%;
540 left: 50%;
541 transform: translate(-50%, -50%);
542 width: 10px;
543 height: 10px;
544 background: #ffffff;
545 border-radius: 50%;
546 opacity: 0;
547 transition: opacity 0.2s ease;
548 }
549
550 .method-option.selected .checkmark {
551 background: #10b981;
552 border-color: #10b981;
553 }
554
555 .method-option.selected .checkmark::after {
556 opacity: 1;
557 }
558
559
560
561 /* Message Area */
562 .message-area {
563 margin-bottom: 24px;
564 min-height: 20px;
565 }
566
567 .payment-message {
568 padding: 16px 20px;
569 border-radius: 8px;
570 font-size: 14px;
571 display: flex;
572 align-items: center;
573 gap: 12px;
574 border: 1px solid;
575 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
576 }
577
578 .payment-message.success {
579 background: #f0fdf4;
580 color: #166534;
581 border-color: #bbf7d0;
582 }
583
584 .payment-message.error {
585 background: #fef2f2;
586 color: #dc2626;
587 border-color: #fecaca;
588 }
589
590 .payment-message.warning {
591 background: #fffbeb;
592 color: #d97706;
593 border-color: #fed7aa;
594 }
595
596 .payment-message.info {
597 background: #eff6ff;
598 color: #1d4ed8;
599 border-color: #bfdbfe;
600 }
601
602
603
604 /* Submit Section */
605 .submit-section {
606 margin-top: auto;
607 padding-top: 24px;
608 border-top: 1px solid #f3f4f6;
609 }
610
611 .submit-button {
612 width: 100%;
613 padding: 18px 24px;
614 background: linear-gradient(135deg, #10b981 0%, #059669 100%);
615 color: #ffffff;
616 border: none;
617 border-radius: 12px;
618 font-size: 16px;
619 font-weight: 600;
620 cursor: pointer;
621 transition: all 0.3s ease;
622 display: flex;
623 align-items: center;
624 justify-content: center;
625 gap: 10px;
626 position: relative;
627 box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
628 }
629
630 .submit-button:hover:not(:disabled) {
631 background: linear-gradient(135deg, #059669 0%, #047857 100%);
632 transform: translateY(-2px);
633 box-shadow: 0 8px 20px rgba(16, 185, 129, 0.4);
634 }
635
636 .submit-button:active:not(:disabled) {
637 transform: translateY(0);
638 box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
639 }
640
641 .submit-button:disabled {
642 background: #9ca3af;
643 cursor: not-allowed;
644 transform: none;
645 box-shadow: none;
646 }
647
648 .button-loader {
649 display: flex;
650 align-items: center;
651 gap: 10px;
652 }
653
654 .button-loader.hidden {
655 display: none;
656 }
657
658 .loader {
659 width: 18px;
660 height: 18px;
661 border: 2px solid rgba(255, 255, 255, 0.3);
662 border-top: 2px solid #ffffff;
663 border-radius: 50%;
664 animation: spin 1s linear infinite;
665 }
666
667 @keyframes spin {
668 0% { transform: rotate(0deg); }
669 100% { transform: rotate(360deg); }
670 }
671
672 .security-note {
673 display: flex;
674 align-items: center;
675 justify-content: center;
676 gap: 8px;
677 margin-top: 16px;
678 font-size: 13px;
679 color: #6b7280;
680 padding: 12px;
681 background: #f9fafb;
682 border-radius: 8px;
683 }
684
685 .security-note i {
686 font-size: 14px;
687 color: #10b981;
688 }
689
690 /* Responsive */
691 @media (max-width: 768px) {
692 .easy-invoice-payment-sidebar {
693 width: 100%;
694 height: 100vh;
695 }
696
697 .payment-header {
698 padding: 24px 20px 20px;
699 }
700
701 .payment-form {
702 padding: 20px;
703 }
704
705 .method-label {
706 padding: 16px;
707 }
708
709 .submit-button {
710 padding: 16px 20px;
711 }
712 }
713
714 /* Scrollbar */
715 .payment-form::-webkit-scrollbar {
716 width: 6px;
717 }
718
719 .payment-form::-webkit-scrollbar-track {
720 background: #f1f5f9;
721 border-radius: 3px;
722 }
723
724 .payment-form::-webkit-scrollbar-thumb {
725 background: #cbd5e1;
726 border-radius: 3px;
727 }
728
729 .payment-form::-webkit-scrollbar-thumb:hover {
730 background: #94a3b8;
731 }
732
733
734
735 /* Utility */
736 .hidden {
737 display: none !important;
738 }
739 </style>
740
741 <script>
742 document.addEventListener('DOMContentLoaded', function() {
743 const paymentForm = document.getElementById('easy-invoice-payment-form');
744 const payButton = document.getElementById('pay-now-button');
745 const messageArea = document.getElementById('payment-message-area');
746 const paymentMethods = document.querySelectorAll('input[name="payment_method_radio"]');
747 const paymentAmountInput = document.getElementById('payment_amount');
748
749 window.selectedGateway = null;
750
751 // Initialize payment methods
752 paymentMethods.forEach(method => {
753 method.addEventListener('change', handlePaymentMethodChange);
754 });
755
756 // Handle payment amount changes
757 if (paymentAmountInput) {
758 paymentAmountInput.addEventListener('input', updatePayButtonText);
759 paymentAmountInput.addEventListener('change', updatePayButtonText);
760 }
761
762 // Initialize button text on page load (always call this)
763 updatePayButtonText();
764
765 // Also call it after a short delay to ensure all elements are loaded
766 setTimeout(() => {
767 updatePayButtonText();
768 }, 500);
769
770 // Handle form submission
771 paymentForm.addEventListener('submit', handlePaymentSubmit);
772
773 function handlePaymentMethodChange(e) {
774 window.selectedGateway = e.target.value;
775 const methodEntry = e.target.closest('.method-option');
776
777 // Update UI
778 document.querySelectorAll('.method-option').forEach(entry => {
779 entry.classList.remove('selected');
780 });
781 methodEntry.classList.add('selected');
782
783 // Update hidden payment_method field
784 const paymentMethodField = document.getElementById('payment_method');
785 if (paymentMethodField) {
786 paymentMethodField.value = window.selectedGateway;
787 }
788
789 // Enable pay button
790 payButton.disabled = false;
791
792 // Load gateway-specific content
793 loadGatewayContent(window.selectedGateway);
794 }
795
796 function loadGatewayContent(gateway) {
797 // Find the specific gateway content div for this gateway
798 const contentDiv = document.getElementById('gateway-content-' + gateway);
799
800 if (!contentDiv) {
801 console.error('Gateway content div not found for:', gateway);
802 return;
803 }
804
805 // Hide all gateway content divs first
806 document.querySelectorAll('.gateway-content').forEach(div => {
807 div.style.display = 'none';
808 });
809
810 // Clear existing content
811 contentDiv.innerHTML = '';
812
813
814 // Load gateway content via AJAX
815 const formData = new FormData();
816 formData.append('action', 'easy_invoice_get_payment_instructions');
817 formData.append('gateway', gateway);
818 formData.append('invoice_id', document.querySelector('input[name="invoice_id"]').value);
819 formData.append('nonce', document.querySelector('input[name="payment_nonce"]').value);
820 // Per-invoice access token from the emailed link (?ik=...); the server
821 // authorises the request on it. Without it an anonymous client holding a
822 // legitimate link could not load their own payment instructions.
823 formData.append('access_token', <?php echo wp_json_encode($ei_ps_access_token); ?>);
824
825 fetch('<?php echo esc_url(admin_url('admin-ajax.php')); ?>', {
826 method: 'POST',
827 body: formData
828 })
829 .then(response => {
830 if (!response.ok) {
831 throw new Error('Network response was not ok');
832 }
833 return response.json();
834 })
835 .then(response => {
836 if (response.success && response.data.instructions) {
837 contentDiv.innerHTML = response.data.instructions;
838 contentDiv.style.display = 'block';
839 } else {
840 contentDiv.style.display = 'none';
841 }
842 })
843 .catch(error => {
844 console.error('Error loading gateway content:', error);
845 contentDiv.style.display = 'none';
846 });
847 }
848
849 function loadGatewayContentViaAjax(gateway, contentDiv) {
850 const formData = new FormData();
851 formData.append('action', 'easy_invoice_get_payment_instructions');
852 formData.append('gateway', gateway);
853 formData.append('invoice_id', document.querySelector('input[name="invoice_id"]').value);
854 formData.append('nonce', document.querySelector('input[name="payment_nonce"]').value);
855 formData.append('access_token', <?php echo wp_json_encode($ei_ps_access_token); ?>);
856
857 fetch('<?php echo esc_url(admin_url('admin-ajax.php')); ?>', {
858 method: 'POST',
859 body: formData
860 })
861 .then(response => response.json())
862 .then(response => {
863 if (response.success && response.data.instructions) {
864 contentDiv.innerHTML = response.data.instructions;
865 contentDiv.style.display = 'block';
866 } else {
867 contentDiv.style.display = 'none';
868 }
869 })
870 .catch(error => {
871 console.error('Error loading gateway content:', error);
872 contentDiv.style.display = 'none';
873 });
874 }
875
876
877
878 function handlePaymentSubmit(e) {
879 e.preventDefault();
880 payButton.disabled = true;
881
882 if (!window.selectedGateway) {
883 showPaymentMessage('Please select a payment method first.', 'error');
884 payButton.disabled = false;
885 return;
886 }
887
888 // Show loading state
889 payButton.querySelector('.button-text').classList.add('hidden');
890 payButton.querySelector('.button-loader').classList.remove('hidden');
891
892 // Handle all payment methods (PayPal, etc.)
893 processStandardPayment();
894 }
895
896
897
898 function processStandardPayment() {
899 const formData = new FormData(paymentForm);
900 formData.append('payment_method', window.selectedGateway);
901 // The per-invoice access token from the emailed link. processPayment()
902 // authorises on it before anything else; without it a client who
903 // arrived by their own link was refused with "Invalid invoice" the
904 // moment they chose bank transfer, cheque or cash.
905 formData.append('access_token', <?php echo wp_json_encode($ei_ps_access_token); ?>);
906
907 // Get AJAX URL from WordPress
908 const ajaxUrl = '<?php echo esc_url(admin_url('admin-ajax.php')); ?>';
909
910 fetch(ajaxUrl, {
911 method: 'POST',
912 body: formData
913 })
914 .then(response => response.json())
915 .then(response => {
916 if (response.success) {
917 handlePaymentSuccess(response.data);
918 } else {
919 const errorMsg = response.data && response.data.message
920 ? response.data.message
921 : 'Payment processing failed.';
922 showPaymentMessage(errorMsg, 'error');
923 resetPayButton();
924 }
925 })
926 .catch(error => {
927 console.error('Payment AJAX error:', error);
928 showPaymentMessage('Server error during payment. Please try again.', 'error');
929 resetPayButton();
930 });
931 }
932
933
934
935 function handlePaymentSuccess(data) {
936 // Handle redirect if provided (for PayPal and other external gateways)
937 if (data.redirect_url) {
938 // Get gateway-specific message or use default
939 const redirectMessage = data.message || 'Redirecting to payment gateway to complete your payment...';
940
941 // Update button state to show redirecting
942 payButton.querySelector('.button-text').textContent = 'Processing...';
943 payButton.querySelector('.button-loader').classList.add('hidden');
944 payButton.querySelector('.button-text').classList.remove('hidden');
945 payButton.disabled = true;
946
947 // Show redirect message
948 showPaymentMessage(redirectMessage, 'info');
949
950 // Redirect immediately
951 setTimeout(() => {
952 window.location.href = data.redirect_url;
953 }, 1000);
954 return;
955 }
956
957 // For successful payments without redirect
958 // Update button state
959 payButton.querySelector('.button-text').textContent = 'Payment Successful!';
960 payButton.querySelector('.button-loader').classList.add('hidden');
961 payButton.querySelector('.button-text').classList.remove('hidden');
962 payButton.disabled = true;
963
964 // Show success message
965 showPaymentMessage(data.message || 'Payment successful!', 'success');
966
967 // Reload the page after delay
968 setTimeout(() => {
969 window.location.reload();
970 }, 2000);
971 }
972
973 function showPaymentMessage(message, type) {
974 let icon = 'info-circle';
975 if (type === 'success') {
976 icon = 'check-circle';
977 } else if (type === 'error') {
978 icon = 'exclamation-triangle';
979 } else if (type === 'warning') {
980 icon = 'exclamation-triangle';
981 }
982
983 messageArea.innerHTML = `
984 <div class="payment-message ${type}">
985 ${message}
986 </div>
987 `;
988 }
989
990 function updatePayButtonText() {
991 // No button when the panel has no payment methods to offer.
992 if (!payButton || !payButton.querySelector('.button-text')) { return; }
993 const hiddenPaymentAmount = document.getElementById('payment_amount');
994 const paymentAmount = hiddenPaymentAmount ? hiddenPaymentAmount.value : '0';
995 const money = <?php echo wp_json_encode(easy_invoice_money_format_spec($invoice)); ?>;
996 const payLabel = <?php echo wp_json_encode(__('Pay', 'easy-invoice')); ?>;
997
998 if (paymentAmount && paymentAmount > 0) {
999 // Same rules as the PHP formatter: separators, precision, symbol position.
1000 const n = parseFloat(paymentAmount).toFixed(money.precision);
1001 const [int, dec] = n.split('.');
1002 const grouped = int.replace(/\B(?=(\d{3})+(?!\d))/g, money.thousands);
1003 const number = dec !== undefined ? grouped + money.decimal + dec : grouped;
1004 const formatted = (money.position === 'after' || money.position === 'right') ? number + money.symbol
1005 : (money.position === 'right_space') ? number + ' ' + money.symbol
1006 : (money.position === 'left_space') ? money.symbol + ' ' + number
1007 : money.symbol + number;
1008 payButton.querySelector('.button-text').textContent = `${payLabel} ${formatted}`;
1009 } else {
1010 payButton.querySelector('.button-text').textContent = 'Pay';
1011 }
1012 }
1013
1014 // Make the function globally accessible
1015 window.updatePayButtonText = updatePayButtonText;
1016
1017 function resetPayButton() {
1018 payButton.querySelector('.button-text').textContent = 'Pay Now';
1019 payButton.querySelector('.button-loader').classList.add('hidden');
1020 payButton.querySelector('.button-text').classList.remove('hidden');
1021 payButton.disabled = false;
1022 }
1023
1024
1025
1026
1027 });
1028 </script>
1029
1030 <?php
1031 // Allow Pro plugins to add additional variables
1032 do_action('easy_invoice_payment_form_variables');
1033 ?>