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
← All changes | templates/payment-section.php +83 -24 2.3.42.4.1 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,13 +38,47 @@
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 (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 +?>
33 75
76 +
34 77 <div class="easy-invoice-payment-sidebar">
35 78 <div class="payment-header">
36 79 <div class="header-content">
37 - <h2><?php _e('Payment', 'easy-invoice'); ?></h2>
80 + <h2><?php esc_html_e('Payment', 'easy-invoice'); ?></h2>
38 81 <div class="invoice-summary">
39 82 <span class="invoice-number"><?php echo esc_html($invoice->getNumber()); ?></span>
40 83 <span class="invoice-date"><?php echo esc_html(date_i18n('M j, Y', strtotime($invoice->getIssueDate()))); ?></span>
41 84 </div>
@@ -41,11 +84,11 @@
41 84 </div>
42 85 </div>
43 86
44 87 <div class="amount-display">
45 - <div class="amount-label"><?php _e('Total Amount', 'easy-invoice'); ?></div>
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>
46 89 <div class="amount-value">
47 - <?php echo esc_html($currency_symbol . number_format($total_amount, 2)); ?>
90 + <?php echo esc_html(easy_invoice_format_money($total_amount, $invoice)); ?>
48 91 </div>
49 92 </div>
50 93 </div>
51 94
@@ -52,12 +95,12 @@
52 95 <?php if (empty($available_gateways)): ?>
53 96 <div class="no-payment-methods">
54 97 <div class="empty-state">
55 98 <div class="empty-icon">
56 - <i class="fas fa-credit-card"></i>
99 + <?php echo easy_invoice_payment_icon('card'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
57 100 </div>
58 - <h3><?php _e('No Payment Methods', 'easy-invoice'); ?></h3>
59 - <p><?php _e('Payment methods are not configured for this invoice.', 'easy-invoice'); ?></p>
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>
60 103 </div>
61 104 </div>
62 105 <?php else: ?>
63 106 <form id="easy-invoice-payment-form" class="payment-form">
@@ -75,9 +118,9 @@
75 118
76 119 ?>
77 120
78 121 <div class="payment-methods">
79 - <label class="section-label"><?php _e('Payment Method', 'easy-invoice'); ?></label>
122 + <label class="section-label"><?php esc_html_e('Payment Method', 'easy-invoice'); ?></label>
80 123
81 124 <div class="method-options">
82 125 <?php foreach ($available_gateways as $gateway): ?>
83 126 <div class="method-option" data-gateway="<?php echo esc_attr($gateway['id']); ?>">
@@ -89,13 +132,9 @@
89 132
90 133 <label for="payment_method_<?php echo esc_attr($gateway['id']); ?>" class="method-label">
91 134 <div class="method-content">
92 135 <div class="method-icon">
93 - <?php if (!empty($gateway['icon'])): ?>
94 - <i class="<?php echo esc_attr($gateway['icon']); ?>"></i>
95 - <?php else: ?>
96 - <i class="fas fa-credit-card"></i>
97 - <?php endif; ?>
136 + <?php echo easy_invoice_payment_icon((string) $gateway['id'], (string) ($gateway['icon'] ?? '')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- inline SVG / escaped img. ?>
98 137 </div>
99 138
100 139 <div class="method-details">
101 140 <div class="method-name"><?php echo esc_html($gateway['title']); ?></div>
@@ -119,18 +158,18 @@
119 158 <div class="message-area" id="payment-message-area"></div>
120 159
121 160 <div class="submit-section">
122 161 <button type="submit" id="pay-now-button" class="submit-button" disabled>
123 - <span class="button-text"><?php _e('Pay', 'easy-invoice'); ?> <?php echo esc_html($currency_symbol . number_format($total_amount, 2)); ?></span>
162 + <span class="button-text"><?php esc_html_e('Pay', 'easy-invoice'); ?> <?php echo esc_html(easy_invoice_format_money($total_amount, $invoice)); ?></span>
124 163 <div class="button-loader hidden">
125 164 <div class="loader"></div>
126 - <span><?php _e('Processing...', 'easy-invoice'); ?></span>
165 + <span><?php esc_html_e('Processing...', 'easy-invoice'); ?></span>
127 166 </div>
128 167 </button>
129 168
130 169 <div class="security-note">
131 - <i class="fas fa-lock"></i>
132 - <span><?php _e('Your payment is secure and encrypted', 'easy-invoice'); ?></span>
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>
133 172 </div>
134 173 </div>
135 174 </form>
136 175 <?php endif; ?>
@@ -777,10 +816,14 @@
777 816 formData.append('action', 'easy_invoice_get_payment_instructions');
778 817 formData.append('gateway', gateway);
779 818 formData.append('invoice_id', document.querySelector('input[name="invoice_id"]').value);
780 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); ?>);
781 824
782 - fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
825 + fetch('<?php echo esc_url(admin_url('admin-ajax.php')); ?>', {
783 826 method: 'POST',
784 827 body: formData
785 828 })
786 829 .then(response => {
@@ -808,10 +851,11 @@
808 851 formData.append('action', 'easy_invoice_get_payment_instructions');
809 852 formData.append('gateway', gateway);
810 853 formData.append('invoice_id', document.querySelector('input[name="invoice_id"]').value);
811 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); ?>);
812 856
813 - fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
857 + fetch('<?php echo esc_url(admin_url('admin-ajax.php')); ?>', {
814 858 method: 'POST',
815 859 body: formData
816 860 })
817 861 .then(response => response.json())
@@ -853,11 +897,16 @@
853 897
854 898 function processStandardPayment() {
855 899 const formData = new FormData(paymentForm);
856 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); ?>);
857 906
858 907 // Get AJAX URL from WordPress
859 - const ajaxUrl = '<?php echo admin_url('admin-ajax.php'); ?>';
908 + const ajaxUrl = '<?php echo esc_url(admin_url('admin-ajax.php')); ?>';
860 909
861 910 fetch(ajaxUrl, {
862 911 method: 'POST',
863 912 body: formData
@@ -932,9 +981,8 @@
932 981 }
933 982
934 983 messageArea.innerHTML = `
935 984 <div class="payment-message ${type}">
936 - <i class="fas fa-${icon}"></i>
937 985 ${message}
938 986 </div>
939 987 `;
940 988 }
@@ -939,15 +987,26 @@
939 987 `;
940 988 }
941 989
942 990 function updatePayButtonText() {
991 + // No button when the panel has no payment methods to offer.
992 + if (!payButton || !payButton.querySelector('.button-text')) { return; }
943 993 const hiddenPaymentAmount = document.getElementById('payment_amount');
944 994 const paymentAmount = hiddenPaymentAmount ? hiddenPaymentAmount.value : '0';
945 - const currencySymbol = '<?php echo esc_js($currency_symbol); ?>';
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')); ?>;
946 997
947 998 if (paymentAmount && paymentAmount > 0) {
948 - const formattedAmount = parseFloat(paymentAmount).toFixed(2);
949 - payButton.querySelector('.button-text').textContent = `Pay ${currencySymbol}${formattedAmount}`;
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}`;
950 1009 } else {
951 1010 payButton.querySelector('.button-text').textContent = 'Pay';
952 1011 }
953 1012 }