PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.2
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
← All changes | templates/payment-section.php +142 -81 2.2.0 → 2.4.2 View file →
@@ -7,12 +7,21 @@
7 7 * @package EasyInvoice
8 8 * @since 1.0.0
9 9 */
10 10
11 +if ( ! defined( 'ABSPATH' ) ) {
12 + exit;
13 +}
11 14 // Get invoice data
12 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
13 21 $invoice = new \EasyInvoice\Models\Invoice(get_post($invoice_id));
14 -$total_amount = $invoice->getTotal();
22 +// What is owed now: total less payments received and credit notes.
23 +$total_amount = \EasyInvoice\Services\InvoiceBalance::due($invoice);
15 24 $currency_code = $invoice->getCurrencyCode() ?: 'USD';
16 25
17 26 // If currency is "global", use the global setting
18 27 if ($currency_code === 'global') {
@@ -29,14 +38,48 @@
29 38 $payment_nonce = wp_create_nonce('easy_invoice_payment');
30 39
31 40
32 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 (isset($paths[strtolower($icon)])) { $key = strtolower($icon); }
68 + elseif (false !== strpos($id, 'bank') || false !== strpos($id, 'transfer') || false !== strpos($id, 'wire')) { $key = 'bank'; }
69 + elseif (false !== strpos($id, 'cheque') || false !== strpos($id, 'check')) { $key = 'cheque'; }
70 + elseif (false !== strpos($id, 'cash')) { $key = 'cash'; }
71 + elseif (false !== strpos($id, 'paypal') || false !== strpos($id, 'paystack') || false !== strpos($id, 'mollie')) { $key = 'wallet'; }
72 + return '<svg ' . $a . '>' . $paths[$key] . '</svg>';
73 + }
74 +}
75 +?>
33 76
77 +
34 78 <div class="easy-invoice-payment-sidebar">
35 - <!-- Header -->
36 79 <div class="payment-header">
37 80 <div class="header-content">
38 - <h2><?php _e('Payment', 'easy-invoice'); ?></h2>
81 + <h2><?php esc_html_e('Payment', 'easy-invoice'); ?></h2>
39 82 <div class="invoice-summary">
40 83 <span class="invoice-number"><?php echo esc_html($invoice->getNumber()); ?></span>
41 84 <span class="invoice-date"><?php echo esc_html(date_i18n('M j, Y', strtotime($invoice->getIssueDate()))); ?></span>
42 85 </div>
@@ -42,11 +85,11 @@
42 85 </div>
43 86 </div>
44 87
45 88 <div class="amount-display">
46 - <div class="amount-label"><?php _e('Total Amount', 'easy-invoice'); ?></div>
89 + <div class="amount-label"><?php echo esc_html((float) $invoice->getTotal() - $total_amount > 0.005 ? __('Amount Due', 'easy-invoice') : __('Total Amount', 'easy-invoice')); ?></div>
47 90 <div class="amount-value">
48 - <?php echo esc_html($currency_symbol . number_format($total_amount, 2)); ?>
91 + <?php echo esc_html(easy_invoice_format_money($total_amount, $invoice)); ?>
49 92 </div>
50 93 </div>
51 94 </div>
52 95
@@ -53,12 +96,12 @@
53 96 <?php if (empty($available_gateways)): ?>
54 97 <div class="no-payment-methods">
55 98 <div class="empty-state">
56 99 <div class="empty-icon">
57 - <i class="fas fa-credit-card"></i>
100 + <?php echo easy_invoice_payment_icon('card'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
58 101 </div>
59 - <h3><?php _e('No Payment Methods', 'easy-invoice'); ?></h3>
60 - <p><?php _e('Payment methods are not configured for this invoice.', 'easy-invoice'); ?></p>
102 + <h3><?php esc_html_e('No Payment Methods', 'easy-invoice'); ?></h3>
103 + <p><?php esc_html_e('Payment methods are not configured for this invoice.', 'easy-invoice'); ?></p>
61 104 </div>
62 105 </div>
63 106 <?php else: ?>
64 107 <form id="easy-invoice-payment-form" class="payment-form">
@@ -75,11 +118,10 @@
75 118 do_action('easy_invoice_payment_gateways_before', $invoice);
76 119
77 120 ?>
78 121
79 - <!-- Payment Methods -->
80 122 <div class="payment-methods">
81 - <label class="section-label"><?php _e('Payment Method', 'easy-invoice'); ?></label>
123 + <label class="section-label"><?php esc_html_e('Payment Method', 'easy-invoice'); ?></label>
82 124
83 125 <div class="method-options">
84 126 <?php foreach ($available_gateways as $gateway): ?>
85 127 <div class="method-option" data-gateway="<?php echo esc_attr($gateway['id']); ?>">
@@ -91,13 +133,9 @@
91 133
92 134 <label for="payment_method_<?php echo esc_attr($gateway['id']); ?>" class="method-label">
93 135 <div class="method-content">
94 136 <div class="method-icon">
95 - <?php if (!empty($gateway['icon'])): ?>
96 - <i class="<?php echo esc_attr($gateway['icon']); ?>"></i>
97 - <?php else: ?>
98 - <i class="fas fa-credit-card"></i>
99 - <?php endif; ?>
137 + <?php echo easy_invoice_payment_icon((string) $gateway['id'], (string) ($gateway['icon'] ?? '')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- inline SVG / escaped img. ?>
100 138 </div>
101 139
102 140 <div class="method-details">
103 141 <div class="method-name"><?php echo esc_html($gateway['title']); ?></div>
@@ -111,9 +149,8 @@
111 149 </div>
112 150 </div>
113 151 </label>
114 152
115 - <!-- Gateway-specific content will be loaded here -->
116 153 <div class="gateway-content" id="gateway-content-<?php echo esc_attr($gateway['id']); ?>" style="display: none;"></div>
117 154 </div>
118 155 <?php endforeach; ?>
119 156 </div>
@@ -118,24 +155,22 @@
118 155 <?php endforeach; ?>
119 156 </div>
120 157 </div>
121 158
122 - <!-- Messages -->
123 159 <div class="message-area" id="payment-message-area"></div>
124 160
125 - <!-- Submit Button -->
126 161 <div class="submit-section">
127 162 <button type="submit" id="pay-now-button" class="submit-button" disabled>
128 - <span class="button-text"><?php _e('Pay', 'easy-invoice'); ?> <?php echo esc_html($currency_symbol . number_format($total_amount, 2)); ?></span>
163 + <span class="button-text"><?php esc_html_e('Pay', 'easy-invoice'); ?> <?php echo esc_html(easy_invoice_format_money($total_amount, $invoice)); ?></span>
129 164 <div class="button-loader hidden">
130 165 <div class="loader"></div>
131 - <span><?php _e('Processing...', 'easy-invoice'); ?></span>
166 + <span><?php esc_html_e('Processing...', 'easy-invoice'); ?></span>
132 167 </div>
133 168 </button>
134 169
135 170 <div class="security-note">
136 - <i class="fas fa-lock"></i>
137 - <span><?php _e('Your payment is secure and encrypted', 'easy-invoice'); ?></span>
171 + <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>
172 + <span><?php esc_html_e('Your payment is secure and encrypted', 'easy-invoice'); ?></span>
138 173 </div>
139 174 </div>
140 175 </form>
141 176 <?php endif; ?>
@@ -322,8 +357,24 @@
322 357 margin-top: 8px;
323 358 font-size: 13px;
324 359 }
325 360
361 +.gateway-content .ei-offline h4 { margin: 0 0 10px; font-size: 15px; font-weight: 600; color: #111827; }
362 +.gateway-content .ei-offline__details { width: 100%; border-collapse: collapse; margin-bottom: 12px; font-size: 13px; }
363 +.gateway-content .ei-offline__details th { text-align: left; padding: 6px 8px 6px 0; color: #6b7280; font-weight: 500; width: 40%; vertical-align: top; }
364 +.gateway-content .ei-offline__details td { padding: 6px 0; color: #111827; font-weight: 600; word-break: break-word; }
365 +.gateway-content .ei-offline__instructions { font-size: 13px; color: #374151; line-height: 1.5; margin-bottom: 10px; }
366 +.gateway-content .ei-offline__instructions p { margin: 0 0 8px; }
367 +.gateway-content .ei-offline__form { margin-top: 14px; padding-top: 14px; border-top: 1px solid #e5e7eb; }
368 +.gateway-content .ei-offline__lead { font-size: 13px; color: #374151; margin: 0 0 10px; }
369 +.gateway-content .ei-offline__form .form-group { margin-bottom: 12px; }
370 +.gateway-content .ei-offline__form label { display: block; font-size: 13px; font-weight: 600; color: #111827; margin-bottom: 4px; }
371 +.gateway-content .ei-offline__optional { font-weight: 400; color: #6b7280; }
372 +.gateway-content .ei-offline__required { color: #dc2626; }
373 +.gateway-content .ei-offline__form input[type="text"],
374 +.gateway-content .ei-offline__form textarea { width: 100%; box-sizing: border-box; border: 1px solid #d1d5db; border-radius: 8px; padding: 9px 12px; font-size: 14px; background: #fff; }
375 +.gateway-content .ei-offline__form input[type="file"] { display: block; width: 100%; font-size: 13px; color: #374151; }
376 +.gateway-content .ei-offline__form .reference-note { margin: 4px 0 0; font-size: 12px; color: #6b7280; }
326 377 .gateway-content .payment-proof-form {
327 378 margin-top: 24px;
328 379 padding-top: 20px;
329 380 border-top: 1px solid #e5e7eb;
@@ -730,9 +781,8 @@
730 781
731 782 // Also call it after a short delay to ensure all elements are loaded
732 783 setTimeout(() => {
733 784 updatePayButtonText();
734 - console.log('updatePayButtonText called on page load (delayed)');
735 785 }, 500);
736 786
737 787 // Handle form submission
738 788 paymentForm.addEventListener('submit', handlePaymentSubmit);
@@ -739,12 +789,9 @@
739 789
740 790 function handlePaymentMethodChange(e) {
741 791 window.selectedGateway = e.target.value;
742 792 const methodEntry = e.target.closest('.method-option');
743 -
744 - console.log('Payment method changed to:', window.selectedGateway);
745 - console.log('Pay button before:', payButton.disabled);
746 -
793 +
747 794 // Update UI
748 795 document.querySelectorAll('.method-option').forEach(entry => {
749 796 entry.classList.remove('selected');
750 797 });
@@ -757,10 +804,9 @@
757 804 }
758 805
759 806 // Enable pay button
760 807 payButton.disabled = false;
761 - console.log('Pay button after:', payButton.disabled);
762 -
808 +
763 809 // Load gateway-specific content
764 810 loadGatewayContent(window.selectedGateway);
765 811 }
766 812
@@ -782,24 +828,23 @@
782 828 contentDiv.innerHTML = '';
783 829
784 830
785 831 // Load gateway content via AJAX
786 - console.log('Loading gateway content for:', gateway);
787 - console.log('Content div:', contentDiv);
788 -
789 - // Get form data for the request
790 832 const formData = new FormData();
791 833 formData.append('action', 'easy_invoice_get_payment_instructions');
792 834 formData.append('gateway', gateway);
793 835 formData.append('invoice_id', document.querySelector('input[name="invoice_id"]').value);
794 836 formData.append('nonce', document.querySelector('input[name="payment_nonce"]').value);
795 -
796 - fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
837 + // Per-invoice access token from the emailed link (?ik=...); the server
838 + // authorises the request on it. Without it an anonymous client holding a
839 + // legitimate link could not load their own payment instructions.
840 + formData.append('access_token', <?php echo wp_json_encode($ei_ps_access_token); ?>);
841 +
842 + fetch('<?php echo esc_url(admin_url('admin-ajax.php')); ?>', {
797 843 method: 'POST',
798 844 body: formData
799 845 })
800 846 .then(response => {
801 - console.log('Response status:', response.status);
802 847 if (!response.ok) {
803 848 throw new Error('Network response was not ok');
804 849 }
805 850 return response.json();
@@ -804,15 +849,13 @@
804 849 }
805 850 return response.json();
806 851 })
807 852 .then(response => {
808 - console.log('Response:', response);
809 853 if (response.success && response.data.instructions) {
810 - console.log('Instructions found, displaying content');
811 854 contentDiv.innerHTML = response.data.instructions;
812 855 contentDiv.style.display = 'block';
856 + applyOfflineState(contentDiv);
813 857 } else {
814 - console.log('No instructions found, hiding content');
815 858 contentDiv.style.display = 'none';
816 859 }
817 860 })
818 861 .catch(error => {
@@ -821,47 +864,30 @@
821 864 });
822 865 }
823 866
824 867 function loadGatewayContentViaAjax(gateway, contentDiv) {
825 - console.log('Loading gateway content via AJAX for:', gateway);
826 -
827 - // Get form data for the request
828 868 const formData = new FormData();
829 869 formData.append('action', 'easy_invoice_get_payment_instructions');
830 870 formData.append('gateway', gateway);
831 871 formData.append('invoice_id', document.querySelector('input[name="invoice_id"]').value);
832 872 formData.append('nonce', document.querySelector('input[name="payment_nonce"]').value);
833 -
834 - // Fetch instructions from the server
835 - fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
873 + formData.append('access_token', <?php echo wp_json_encode($ei_ps_access_token); ?>);
874 +
875 + fetch('<?php echo esc_url(admin_url('admin-ajax.php')); ?>', {
836 876 method: 'POST',
837 877 body: formData
838 878 })
879 + .then(response => response.json())
839 880 .then(response => {
840 - console.log('Response status:', response.status);
841 - return response.json();
842 - })
843 - .then(response => {
844 - console.log('Response:', response);
845 881 if (response.success && response.data.instructions) {
846 - console.log('Content div before insertion:', contentDiv);
847 - console.log('Instructions to insert:', response.data.instructions);
848 882 contentDiv.innerHTML = response.data.instructions;
849 - console.log('Content div after insertion:', contentDiv);
850 - console.log('Content div innerHTML:', contentDiv.innerHTML);
851 - console.log('Content div display style:', contentDiv.style.display);
852 - console.log('Instructions loaded successfully');
853 - // Show the content div only after successful AJAX response
854 883 contentDiv.style.display = 'block';
855 884 } else {
856 - console.log('No instructions received for gateway:', gateway);
857 - console.log('Hiding gateway content div');
858 885 contentDiv.style.display = 'none';
859 886 }
860 887 })
861 888 .catch(error => {
862 889 console.error('Error loading gateway content:', error);
863 - console.log('Hiding gateway content div due to error');
864 890 contentDiv.style.display = 'none';
865 891 });
866 892 }
867 893
@@ -866,8 +892,23 @@
866 892 }
867 893
868 894
869 895
896 + // An offline method (bank transfer, cheque, cash, …) is not charged here:
897 + // the button becomes "Submit payment details" and a required receipt is
898 + // checked before anything is sent.
899 + window.eiOfflineSelected = false;
900 + function applyOfflineState(contentDiv) {
901 + var offline = contentDiv ? contentDiv.querySelector('[data-ei-offline]') : null;
902 + window.eiOfflineSelected = !!offline;
903 + if (!payButton || !payButton.querySelector('.button-text')) { return; }
904 + if (offline) {
905 + payButton.querySelector('.button-text').textContent = offline.getAttribute('data-ei-submit-label') || 'Submit payment details';
906 + } else {
907 + updatePayButtonText();
908 + }
909 + }
910 +
870 911 function handlePaymentSubmit(e) {
871 912 e.preventDefault();
872 913 payButton.disabled = true;
873 914
@@ -875,8 +916,17 @@
875 916 showPaymentMessage('Please select a payment method first.', 'error');
876 917 payButton.disabled = false;
877 918 return;
878 919 }
920 + if (window.eiOfflineSelected) {
921 + var required = paymentForm.querySelector('input[type="file"][data-ei-required]');
922 + if (required && !(required.files && required.files.length)) {
923 + showPaymentMessage(<?php echo wp_json_encode(__('Please attach your receipt or proof of payment.', 'easy-invoice')); ?>, 'error');
924 + payButton.disabled = false;
925 + required.focus();
926 + return;
927 + }
928 + }
879 929
880 930 // Show loading state
881 931 payButton.querySelector('.button-text').classList.add('hidden');
882 932 payButton.querySelector('.button-loader').classList.remove('hidden');
@@ -887,20 +937,18 @@
887 937
888 938
889 939
890 940 function processStandardPayment() {
891 - console.log('Processing standard payment');
892 -
893 - // Get form data
894 941 const formData = new FormData(paymentForm);
895 -
896 - // Add payment method to form data
897 942 formData.append('payment_method', window.selectedGateway);
898 -
899 - console.log('Form data:', Object.fromEntries(formData));
900 -
943 + // The per-invoice access token from the emailed link. processPayment()
944 + // authorises on it before anything else; without it a client who
945 + // arrived by their own link was refused with "Invalid invoice" the
946 + // moment they chose bank transfer, cheque or cash.
947 + formData.append('access_token', <?php echo wp_json_encode($ei_ps_access_token); ?>);
948 +
901 949 // Get AJAX URL from WordPress
902 - const ajaxUrl = '<?php echo admin_url('admin-ajax.php'); ?>';
950 + const ajaxUrl = '<?php echo esc_url(admin_url('admin-ajax.php')); ?>';
903 951
904 952 fetch(ajaxUrl, {
905 953 method: 'POST',
906 954 body: formData
@@ -947,8 +995,17 @@
947 995 }, 1000);
948 996 return;
949 997 }
950 998
999 + if (data.offline) {
1000 + payButton.querySelector('.button-loader').classList.add('hidden');
1001 + payButton.querySelector('.button-text').classList.remove('hidden');
1002 + payButton.querySelector('.button-text').textContent = <?php echo wp_json_encode(__('Submitted', 'easy-invoice')); ?>;
1003 + payButton.disabled = true;
1004 + showPaymentMessage(data.message || 'Thank you — we will confirm your payment shortly.', 'success');
1005 + setTimeout(function () { window.location.reload(); }, 2500);
1006 + return;
1007 + }
951 1008 // For successful payments without redirect
952 1009 // Update button state
953 1010 payButton.querySelector('.button-text').textContent = 'Payment Successful!';
954 1011 payButton.querySelector('.button-loader').classList.add('hidden');
@@ -975,9 +1032,8 @@
975 1032 }
976 1033
977 1034 messageArea.innerHTML = `
978 1035 <div class="payment-message ${type}">
979 - <i class="fas fa-${icon}"></i>
980 1036 ${message}
981 1037 </div>
982 1038 `;
983 1039 }
@@ -982,24 +1038,29 @@
982 1038 `;
983 1039 }
984 1040
985 1041 function updatePayButtonText() {
986 - // Get payment amount from the hidden field (updated by partial payments)
1042 + // No button when the panel has no payment methods to offer.
1043 + if (!payButton || !payButton.querySelector('.button-text')) { return; }
1044 + if (window.eiOfflineSelected) { return; }
987 1045 const hiddenPaymentAmount = document.getElementById('payment_amount');
988 1046 const paymentAmount = hiddenPaymentAmount ? hiddenPaymentAmount.value : '0';
989 - const currencySymbol = '<?php echo esc_js($currency_symbol); ?>';
990 -
991 - console.log('updatePayButtonText called');
992 - console.log('Hidden payment amount:', paymentAmount);
993 - console.log('Currency symbol:', currencySymbol);
994 -
1047 + const money = <?php echo wp_json_encode(easy_invoice_money_format_spec($invoice)); ?>;
1048 + const payLabel = <?php echo wp_json_encode(__('Pay', 'easy-invoice')); ?>;
1049 +
995 1050 if (paymentAmount && paymentAmount > 0) {
996 - const formattedAmount = parseFloat(paymentAmount).toFixed(2);
997 - payButton.querySelector('.button-text').textContent = `Pay ${currencySymbol}${formattedAmount}`;
998 - console.log('Updated button text to:', `Pay ${currencySymbol}${formattedAmount}`);
1051 + // Same rules as the PHP formatter: separators, precision, symbol position.
1052 + const n = parseFloat(paymentAmount).toFixed(money.precision);
1053 + const [int, dec] = n.split('.');
1054 + const grouped = int.replace(/\B(?=(\d{3})+(?!\d))/g, money.thousands);
1055 + const number = dec !== undefined ? grouped + money.decimal + dec : grouped;
1056 + const formatted = (money.position === 'after' || money.position === 'right') ? number + money.symbol
1057 + : (money.position === 'right_space') ? number + ' ' + money.symbol
1058 + : (money.position === 'left_space') ? money.symbol + ' ' + number
1059 + : money.symbol + number;
1060 + payButton.querySelector('.button-text').textContent = `${payLabel} ${formatted}`;
999 1061 } else {
1000 1062 payButton.querySelector('.button-text').textContent = 'Pay';
1001 - console.log('Updated button text to: Pay');
1002 1063 }
1003 1064 }
1004 1065
1005 1066 // Make the function globally accessible