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
← All changes | templates/settings-page.php +416 -243 2.1.0 → 2.4.3 View file →
@@ -10,48 +10,97 @@
10 10 if (!defined('ABSPATH')) {
11 11 exit;
12 12 }
13 13
14 -// Function to dequeue WordPress admin styles and load our custom styles
15 -function easy_invoice_dequeue_admin_styles() {
16 - // Get current screen
17 - $screen = get_current_screen();
18 -
19 - // Check if we're on an Easy Invoice page
20 - if ($screen && property_exists($screen, 'id') && strpos($screen->id, 'easy-invoice') !== false) {
21 - // Remove all WordPress admin styles
22 - global $wp_styles;
23 -
24 - $styles_to_remove = [
25 - 'wp-admin', 'admin-bar', 'colors', 'ie', 'common', 'forms', 'admin-menu',
26 - 'dashboard', 'list-tables', 'edit', 'revisions', 'media', 'themes', 'about',
27 - 'nav-menus', 'widgets', 'site-icon', 'l10n', 'code-editor', 'wp-auth-check',
28 - 'dashicons', 'wp-pointer', 'buttons', 'wp-jquery-ui-dialog', 'wp-color-picker',
29 - 'wp-components', 'wp-edit-post', 'wp-format-library', 'wp-block-library',
30 - 'wp-block-library-theme', 'wp-nux', 'wp-reusable-blocks', 'wp-editor',
31 - 'wp-edit-blocks', 'wp-block-editor',
32 - ];
33 - $styles_to_remove = array_unique($styles_to_remove); // Ensure no duplicates
14 +/**
15 + * Single source of truth for per-section icons.
16 + *
17 + * Used by BOTH the left-side section nav (small icons, h-5 w-5) AND the
18 + * right-side content-panel header (larger icon, h-6 w-6 inside an
19 + * indigo-100 background tile). The nav previously had its own inline
20 + * SVGs and the header used Font Awesome — different glyphs for the
21 + * same section. This helper unifies them so changing the icon for a
22 + * section is a one-line edit here.
23 + *
24 + * @param string $section_id Section slug (company / invoice / quote / etc.)
25 + * @param string $color_class Tailwind classes to apply to the SVG (color + hover).
26 + * @param string $size_class Tailwind size classes (h-5 w-5 for nav, h-6 w-6 for header).
27 + * @param string $extra_class Any extra wrapper classes (e.g. mr-3 for nav alignment).
28 + */
29 +if (!function_exists('easy_invoice_render_settings_section_icon')) {
30 + function easy_invoice_render_settings_section_icon(string $section_id, string $color_class, string $size_class, string $extra_class = ''): void {
31 + $klass = trim($color_class . ' ' . $size_class . ' ' . $extra_class);
32 + $open = '<svg class="' . esc_attr($klass) . '" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">';
33 + $close = '</svg>';
34 34
35 - foreach ($styles_to_remove as $style) {
36 - if (isset($wp_styles->registered[$style])) {
37 - wp_dequeue_style($style);
38 - wp_deregister_style($style);
39 - }
35 + $paths = '';
36 + switch ($section_id) {
37 + case 'company':
38 + // House icon — represents the business / company.
39 + $paths = '<path d="M3 9L12 2L21 9V20C21 20.5304 20.7893 21.0391 20.4142 21.4142C20.0391 21.7893 19.5304 22 19 22H5C4.46957 22 3.96086 21.7893 3.58579 21.4142C3.21071 21.0391 3 20.5304 3 20V9Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
40 + . '<polyline points="9,22 9,12 15,12 15,22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
41 + break;
42 + case 'invoice':
43 + // Document with $ inside — distinct from the quote icon.
44 + $paths = '<path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
45 + . '<path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
46 + . '<path d="M12 11V19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
47 + . '<path d="M14.5 13H10.75a1.75 1.75 0 0 0 0 3.5h2.5a1.75 1.75 0 0 1 0 3.5H9.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
48 + break;
49 + case 'quote':
50 + // Document with checkmark — proposal / awaiting approval.
51 + $paths = '<path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
52 + . '<path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
53 + . '<path d="M8.5 15.5L11 18L16 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
54 + break;
55 + case 'currency':
56 + // Dollar-sign-in-circle.
57 + $paths = '<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
58 + . '<path d="M12 1V23" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
59 + . '<path d="M17 5H9.5C8.57174 5 7.6815 5.36875 7.02513 6.02513C6.36875 6.6815 6 7.57174 6 8.5C6 9.42826 6.36875 10.3185 7.02513 10.9749C7.6815 11.6313 8.57174 12 9.5 12H14.5C15.4283 12 16.3185 12.3687 16.9749 13.0251C17.6313 13.6815 18 14.5717 18 15.5C18 16.4283 17.6313 17.3185 16.9749 17.9749C16.3185 18.6313 15.4283 19 14.5 19H6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
60 + break;
61 + case 'tax':
62 + // Percent symbol — clean tax glyph.
63 + $paths = '<line x1="19" y1="5" x2="5" y2="19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
64 + . '<circle cx="6.5" cy="6.5" r="2.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
65 + . '<circle cx="17.5" cy="17.5" r="2.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
66 + break;
67 + case 'payment':
68 + // Card with horizontal stripe.
69 + $paths = '<rect x="1" y="4" width="22" height="16" rx="2" ry="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
70 + . '<line x1="1" y1="10" x2="23" y2="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
71 + break;
72 + case 'email':
73 + // Envelope with flap.
74 + $paths = '<path d="M4 4H20C21.1 4 22 4.9 22 6V18C22 19.1 21.1 20 20 20H4C2.9 20 2 19.1 2 18V6C2 4.9 2.9 4 4 4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
75 + . '<polyline points="22,6 12,13 2,6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
76 + break;
77 + case 'text_settings':
78 + // Document with text lines + edit-arrow indicator.
79 + $paths = '<path d="M4 7V4C4 3.44772 4.44772 3 5 3H19C19.5523 3 20 3.44772 20 4V7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
80 + . '<path d="M4 7V20C4 20.5523 4.44772 21 5 21H19C19.5523 21 20 20.5523 20 20V7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
81 + . '<path d="M8 11H16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
82 + . '<path d="M8 15H12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
83 + . '<path d="M8 19H14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
84 + . '<path d="M12 11L14 13L12 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
85 + break;
86 + case 'account':
87 + // User in circle — distinct from the gear used elsewhere.
88 + $paths = '<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
89 + . '<circle cx="12" cy="10" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
90 + . '<path d="M6.34 19a6 6 0 0 1 11.32 0" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
91 + break;
92 + case 'advanced':
93 + default:
94 + // Gear icon — used by Advanced + as the fallback for any
95 + // section that didn't ship a case here.
96 + $paths = '<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
97 + . '<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
98 + break;
40 99 }
41 - add_filter('show_admin_bar', '__return_false');
42 -
43 - // Add custom CSS to hide screen-meta-links
44 - echo '<style>
45 - .screen-meta-links,
46 - #screen-meta,
47 - #screen-meta-links {
48 - display: none !important;
49 - }
50 - </style>';
100 + echo $open . $paths . $close; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- inline SVG icon markup assembled from literal path data above.
51 101 }
52 102 }
53 -add_action('admin_enqueue_scripts', 'easy_invoice_dequeue_admin_styles', 9999);
54 103
55 104 /**
56 105 * Helper function to render a single settings field.
57 106 *
@@ -71,18 +120,18 @@
71 120 $placeholder = isset($field_config['placeholder']) ? esc_attr($field_config['placeholder']) : '';
72 121 $required = !empty($field_config['required']) ? 'required' : '';
73 122 $aria_describedby = !empty($description_text) ? 'aria-describedby="' . $field_id . '-description"' : '';
74 123
75 - echo '<div class=" ' . $col_span_class . '">'; // Added 'form-group' class for JS show/hide functionality
124 + echo '<div class=" ' . esc_html($col_span_class) . '">'; // Added 'form-group' class for JS show/hide functionality
76 125
77 126 // Special layout for checkboxes with descriptions (e.g., Invoice Numbering type)
78 127 if ($type === 'checkbox' && !empty($description_text) && ($option_key === 'easy_invoice_invoice_numbering')) {
79 128 echo '<div class="flex items-start space-x-3 p-4 bg-gray-50 rounded-lg border border-gray-200">';
80 129 echo ' <div class="flex items-center h-5 mt-0.5">';
81 - echo ' <input type="checkbox" name="' . $field_name . '" id="' . $field_id . '" value="yes" ' . checked($value, 'yes', false) . ' class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" ' . $required . '>';
130 + echo ' <input type="checkbox" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="yes" ' . checked($value, 'yes', false) . ' class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" ' . esc_html($required) . '>';
82 131 echo ' </div>';
83 132 echo ' <div class="flex-1">';
84 - echo ' <label for="' . $field_id . '" class="block text-sm font-medium text-gray-700 mb-1">' . $label . '</label>';
133 + echo ' <label for="' . $field_id . '" class="block text-sm font-medium text-gray-700 mb-1">' . esc_html($label) . '</label>';
85 134 echo ' <p id="' . $field_id . '-description" class="text-sm text-gray-400 leading-relaxed">' . wp_kses($description_text, array(
86 135 'a' => array(
87 136 'href' => array(),
88 137 'target' => array(),
@@ -92,34 +141,33 @@
92 141 echo ' </div>';
93 142 echo '</div>';
94 143 } else {
95 144 // Standard label for other types, or checkboxes without the special description structure
96 - if ($type !== 'checkbox') {
97 - echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700 mb-2">' . $label . '</label>';
145 + if ($type !== 'checkbox') {
146 + echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700 mb-2">' . esc_html($label) . '</label>';
98 147 }
99 148
100 149 $input_class = 'mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm';
101 150 $select_class = 'mt-1 block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md';
102 151
103 -
104 152 switch ($type) {
105 153 case 'text':
106 154 case 'email':
107 155 case 'url':
108 156 case 'tel':
109 - echo '<input type="' . esc_attr($type) . '" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($value) . '" class="' . $input_class . '" placeholder="' . $placeholder . '" ' . $required . ' ' . $aria_describedby . '>';
157 + echo '<input type="' . esc_attr($type) . '" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '" class="' . esc_html($input_class) . '" placeholder="' . esc_html($placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
110 158 break;
111 159 case 'number':
112 160 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
113 161 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
114 162 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
115 - echo '<input type="number" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($value) . '"' . $min . $max . $step . ' class="' . $input_class . '" placeholder="' . $placeholder . '" ' . $required . ' ' . $aria_describedby . '>';
163 + echo '<input type="number" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '"' . $min . $max . $step . ' class="' . esc_html($input_class) . '" placeholder="' . esc_html($placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
116 164 break;
117 165 case 'textarea':
118 - echo '<textarea id="' . $field_id . '" name="' . $field_name . '" rows="3" class="' . $input_class . '" placeholder="' . $placeholder . '" ' . $required . ' ' . $aria_describedby . '>' . wp_kses_post($value) . '</textarea>';
166 + echo '<textarea id="' . $field_id . '" name="' . esc_html($field_name) . '" rows="3" class="' . esc_html($input_class) . '" placeholder="' . esc_html($placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>' . wp_kses_post($value) . '</textarea>';
119 167 break;
120 168 case 'select':
121 - echo '<select id="' . $field_id . '" name="' . $field_name . '" class="' . $select_class . '" ' . $required . ' ' . $aria_describedby . '>';
169 + echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '" class="' . esc_html($select_class) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
122 170 if (!empty($field_config['options']) && is_array($field_config['options'])) {
123 171 foreach ($field_config['options'] as $opt_val => $opt_label) {
124 172 echo '<option value="' . esc_attr($opt_val) . '" ' . selected($value, $opt_val, false) . '>' . esc_html($opt_label) . '</option>';
125 173 }
@@ -126,25 +174,25 @@
126 174 }
127 175 echo '</select>';
128 176 break;
129 177 case 'multiselect':
130 - echo '<select id="' . $field_id . '" name="' . $field_name . '[]" class="' . $select_class . '" multiple="multiple" aria-label="' . esc_attr($label) . '">';
178 + echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '[]" class="' . esc_html($select_class) . '" multiple="multiple" aria-label="' . esc_attr($label) . '">';
131 179 if (!empty($field_config['options']) && is_array($field_config['options'])) {
132 180 foreach ($field_config['options'] as $opt_val => $opt_label) {
133 181 $selected = is_array($value) && in_array($opt_val, $value) ? 'selected="selected"' : '';
134 - echo '<option value="' . esc_attr($opt_val) . '" ' . $selected . '>' . esc_html($opt_label) . '</option>';
182 + echo '<option value="' . esc_attr($opt_val) . '" ' . esc_html($selected) . '>' . esc_html($opt_label) . '</option>';
135 183 }
136 184 }
137 185 echo '</select>';
138 186 break;
139 187 case 'readonly':
140 - echo '<input type="text" id="' . $field_id . '" value="' . esc_attr($value) . '" class="' . $input_class . ' bg-gray-50" readonly ' . $aria_describedby . '>';
141 - echo '<input type="hidden" name="' . $field_name . '" value="' . esc_attr($value) . '">';
188 + echo '<input type="text" id="' . $field_id . '" value="' . esc_attr($value) . '" class="' . esc_html($input_class) . ' bg-gray-50" readonly ' . $aria_describedby . '>';
189 + echo '<input type="hidden" name="' . esc_html($field_name) . '" value="' . esc_attr($value) . '">';
142 190 break;
143 191 case 'checkbox': // Checkboxes that don't use the special description layout
144 192 echo '<div class="flex items-center">'; // Original 'tax_enabled' was just this simple structure
145 - echo ' <input type="checkbox" name="' . $field_name . '" id="' . $field_id . '" value="yes" ' . checked($value, 'yes', false) . ' class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" ' . $required . ' ' . $aria_describedby . '>';
146 - echo ' <label for="' . $field_id . '" class="ml-2 block text-sm text-gray-900">' . $label . '</label>';
193 + echo ' <input type="checkbox" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="yes" ' . checked($value, 'yes', false) . ' class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" ' . esc_html($required) . ' ' . $aria_describedby . '>';
194 + echo ' <label for="' . $field_id . '" class="ml-2 block text-sm text-gray-900">' . esc_html($label) . '</label>';
147 195 echo '</div>';
148 196 if (!empty($description_text)) { // Show description below the field
149 197 echo '<p id="' . $field_id . '-description" class="mt-2 text-sm text-gray-400 leading-relaxed">' . wp_kses($description_text, array(
150 198 'a' => array(
@@ -157,14 +205,14 @@
157 205 break;
158 206 case 'image':
159 207 echo '<div class="mt-1 flex items-center">';
160 208 echo ' <span class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100">';
161 - echo ' <img id="' . $field_id . '-preview" src="' . esc_url($value) . '" alt="' . esc_attr($label) . '" class="h-full w-full object-cover' . (empty($value) ? ' hidden' : '') . '">';
209 + echo ' <img id="' . $field_id . '-preview"' . (empty($value) ? '' : ' src="' . esc_url($value) . '"') . ' alt="' . esc_attr($label) . '" class="h-full w-full object-cover' . (empty($value) ? ' hidden' : '') . '">';
162 210 echo ' </span>';
163 211 echo ' <button type="button" id="upload_image_button_' . $field_id . '" class="upload-logo-button ml-5 bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" data-uploader_title="' . esc_attr__('Choose Logo', 'easy-invoice') . '" data-uploader_button_text="' . esc_attr__('Select Logo', 'easy-invoice') . '">';
164 212 echo esc_html__('Change', 'easy-invoice');
165 213 echo '</button>';
166 - echo ' <input type="hidden" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($value) . '" ' . $required . ' ' . $aria_describedby . '>';
214 + echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
167 215 echo '</div>';
168 216 break;
169 217 case 'wp_editor':
170 218 // For wp_editor, the label is usually handled before calling it.
@@ -193,15 +241,15 @@
193 241 echo ' <div id="' . $field_id . '_result" class="mt-2" style="display: none;"></div>';
194 242 echo '</div>';
195 243 break;
196 244 case 'color':
197 - echo '<input type="color" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($value) . '" class="h-10 w-20 border border-gray-300 rounded">';
245 + echo '<input type="color" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '" class="h-10 w-20 border border-gray-300 rounded">';
198 246 break;
199 247 case 'range':
200 248 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
201 249 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
202 250 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
203 - echo '<input type="range" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($value) . '"' . $min . $max . $step . ' class="w-full">';
251 + echo '<input type="range" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '"' . $min . $max . $step . ' class="w-full">';
204 252 echo '<span class="text-sm text-gray-400">' . esc_html($value) . '%</span>';
205 253 break;
206 254 case 'button':
207 255 $button_text = $field_config['button_text'] ?? $label;
@@ -209,9 +257,9 @@
209 257 $button_class_name = 'regenerate-invoice-numbers-button';
210 258 if ($option_key === 'easy_invoice_regenerate_quote_numbers') {
211 259 $button_class_name = 'regenerate-quote-numbers-button';
212 260 }
213 - echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' ' . $button_class_name . '">';
261 + echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' ' . wp_kses_post($button_class_name) . '">';
214 262 echo esc_html($button_text);
215 263 echo '</button>';
216 264 break;
217 265 case 'image_upload':
@@ -216,9 +264,9 @@
216 264 break;
217 265 case 'image_upload':
218 266 echo '<div class="image-upload-wrap">';
219 267 echo '<div class="flex items-center space-x-3">';
220 - echo ' <input type="hidden" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($value) . '">';
268 + echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '">';
221 269 echo ' <button type="button" class="upload-image-button bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" data-target="' . $field_id . '">';
222 270 echo esc_html__('Upload Image', 'easy-invoice');
223 271 echo '</button>';
224 272 echo '</div>';
@@ -243,9 +291,9 @@
243 291 echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' regenerate-quote-numbers-button">';
244 292 echo esc_html($button_text);
245 293 echo '</button>';
246 294 } else {
247 - echo '<input type="text" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($value) . '" class="' . $input_class . '" placeholder="' . $placeholder . '" ' . $required . ' ' . $aria_describedby . '>';
295 + echo '<input type="text" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($value) . '" class="' . esc_html($input_class) . '" placeholder="' . esc_html($placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
248 296 }
249 297 break;
250 298 }
251 299 // General description for non-checkbox and non-wp_editor fields (if it exists and not handled by special layouts)
@@ -263,12 +311,13 @@
263 311 }
264 312
265 313 /**
266 314 * Filter settings config to allow extensions to modify it
315 + * Note: This filter is already applied in the controller, so this is redundant
267 316 */
268 -$settings_config = apply_filters('easy_invoice_settings_fields_config', $settings_config);
317 +// $settings_config = apply_filters('easy_invoice_settings_fields_config', $settings_config);
269 318
270 -$active_section = $_GET['section'] ?? key($settings_config);
319 +$active_section = isset($_GET['section']) ? sanitize_key(wp_unslash($_GET['section'])) : key($settings_config); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
271 320 if (!isset($settings_config[$active_section])) {
272 321 $active_section = key($settings_config);
273 322 }
274 323 ?>
@@ -273,10 +322,10 @@
273 322 }
274 323 ?>
275 324
276 325 <div class="p-8">
277 - <!-- Header with Actions -->
278 - <div class="bg-white border-b border-gray-200 px-6 py-5" style="margin-left: -2rem; margin-top: -2rem; padding-left:2rem; padding-right:2rem; margin-right: -2rem;">
326 +
327 + <div class="ei-app-page-header bg-white border-b border-gray-200 px-6 easy-invoice-admin-page-header">
279 328 <div class="flex items-center justify-between">
280 329 <div>
281 330 <h1 class="text-2xl font-bold text-gray-900"><?php esc_html_e('Settings', 'easy-invoice'); ?></h1>
282 331 <p class="mt-1 text-sm text-gray-400"><?php esc_html_e('Configure your invoice management system', 'easy-invoice'); ?></p>
@@ -289,105 +338,48 @@
289 338 </div>
290 339 </div>
291 340 </div>
292 341
293 - <!-- Main Content -->
342 +
294 343 <div class="max-w-7xl mx-auto">
295 344 <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mt-8">
296 - <!-- Left Sidebar - Navigation -->
345 +
297 346 <div class="lg:col-span-1">
298 347 <div class="bg-white shadow-lg rounded-lg sticky top-24 border border-gray-200">
299 348 <nav class="space-y-2 p-4" aria-label="<?php esc_attr_e('Settings Navigation', 'easy-invoice'); ?>">
300 349 <?php foreach ($settings_config as $section_id => $section_data) : ?>
301 350 <div class="space-y-1">
302 - <a href="?page=easy-invoice-settings&section=<?php echo esc_attr($section_id); ?>"
303 - class="nav-link group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200 <?php echo ($section_id === $active_section) ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"
351 + <a href="?page=easy-invoice-settings&section=<?php echo esc_attr($section_id); ?>"
352 + class="nav-link group flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-all duration-200 <?php echo ($section_id === $active_section) ? 'bg-indigo-50 text-indigo-700 border-r-2 border-indigo-600' : 'text-gray-600 hover:bg-indigo-50 hover:text-indigo-700'; ?>"
304 353 aria-selected="<?php echo $section_id === $active_section ? 'true' : 'false'; ?>"
305 354 role="tab">
306 -
307 - <?php if ($section_id === 'company'): ?>
308 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
309 - <path d="M3 9L12 2L21 9V20C21 20.5304 20.7893 21.0391 20.4142 21.4142C20.0391 21.7893 19.5304 22 19 22H5C4.46957 22 3.96086 21.7893 3.58579 21.4142C3.21071 21.0391 3 20.5304 3 20V9Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
310 - <polyline points="9,22 9,12 15,12 15,22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
311 - </svg>
312 - <?php elseif ($section_id === 'invoice'): ?>
313 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
314 - <path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
315 - <path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
316 - <path d="M16 13H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
317 - <path d="M16 17H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
318 - <path d="M10 9H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
319 - </svg>
320 - <?php elseif ($section_id === 'quote'): ?>
321 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
322 - <path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
323 - <path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
324 - <path d="M9 9H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
325 - <path d="M9 13H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
326 - <path d="M9 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
327 - </svg>
328 - <?php elseif ($section_id === 'currency'): ?>
329 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
330 - <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
331 - <path d="M12 1V23" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
332 - <path d="M17 5H9.5C8.57174 5 7.6815 5.36875 7.02513 6.02513C6.36875 6.6815 6 7.57174 6 8.5C6 9.42826 6.36875 10.3185 7.02513 10.9749C7.6815 11.6313 8.57174 12 9.5 12H14.5C15.4283 12 16.3185 12.3687 16.9749 13.0251C17.6313 13.6815 18 14.5717 18 15.5C18 16.4283 17.6313 17.3185 16.9749 17.9749C16.3185 18.6313 15.4283 19 14.5 19H6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
333 - </svg>
334 - <?php elseif ($section_id === 'tax'): ?>
335 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
336 - <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
337 - <path d="M8 14L16 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
338 - <path d="M8 8L16 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
339 - <path d="M12 6V18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
340 - </svg>
341 - <?php elseif ($section_id === 'payment'): ?>
342 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
343 - <rect x="1" y="4" width="22" height="16" rx="2" ry="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
344 - <line x1="1" y1="10" x2="23" y2="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
345 - </svg>
346 - <?php elseif ($section_id === 'email'): ?>
347 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
348 - <path d="M4 4H20C21.1 4 22 4.9 22 6V18C22 19.1 21.1 20 20 20H4C2.9 20 2 19.1 2 18V6C2 4.9 2.9 4 4 4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
349 - <polyline points="22,6 12,13 2,6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
350 - </svg>
351 - <?php elseif ($section_id === 'advanced'): ?>
352 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
353 - <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
354 - <path d="M19.4 15A1.65 1.65 0 0 0 18 14A6 6 0 0 0 6 14A1.65 1.65 0 0 0 4.6 15A2 2 0 0 0 6 19H18A2 2 0 0 0 19.4 15Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
355 - </svg>
356 - <?php elseif ($section_id === 'text_settings'): ?>
357 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
358 - <path d="M4 7V4C4 3.44772 4.44772 3 5 3H19C19.5523 3 20 3.44772 20 4V7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
359 - <path d="M4 7V20C4 20.5523 4.44772 21 5 21H19C19.5523 21 20 20.5523 20 20V7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
360 - <path d="M8 11H16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
361 - <path d="M8 15H12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
362 - <path d="M8 19H14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
363 - <path d="M12 11L14 13L12 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
364 - </svg>
365 - <?php else: ?>
366 - <svg class="<?php echo ($section_id === $active_section) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-gray-500'; ?> mr-3 flex-shrink-0 h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
367 - <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
368 - <path d="M19.4 15A1.65 1.65 0 0 0 18 14A6 6 0 0 0 6 14A1.65 1.65 0 0 0 4.6 15A2 2 0 0 0 6 19H18A2 2 0 0 0 19.4 15Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
369 - </svg>
370 - <?php endif; ?>
371 -
355 +
356 + <?php
357 + // Section nav icon — same source as the content
358 + // header below. See easy_invoice_render_settings_section_icon().
359 + $is_active = ($section_id === $active_section);
360 + $icon_color = $is_active ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-500';
361 + easy_invoice_render_settings_section_icon($section_id, $icon_color, 'h-5 w-5', 'mr-3 flex-shrink-0');
362 + ?>
363 +
372 364 <span class="flex-1"><?php echo esc_html($section_data['title']); ?></span>
373 365 <?php if ($section_id === 'email' && !empty($section_data['subsections'])): ?>
374 - <svg class="flex-shrink-0 h-4 w-4 text-gray-400 group-hover:text-gray-500 transition-transform duration-200" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
366 + <svg class="flex-shrink-0 h-4 w-4 text-gray-400 group-hover:text-indigo-500 transition-transform duration-200" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
375 367 <polyline points="6,9 12,15 18,9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
376 368 </svg>
377 369 <?php endif; ?>
378 370 </a>
379 -
371 +
380 372 <?php if ($section_id === 'email' && !empty($section_data['subsections'])): ?>
381 373 <div class="ml-6 space-y-1 email-settings-submenu" style="display: none;">
382 - <?php
374 + <?php
383 375 // Only show active subsection if we're actually on an email settings page
384 376 $active_email_subsection = null;
385 377 if ($active_section === 'email') {
386 378 if (isset($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
387 - $active_email_subsection = $_GET['subsection'];
379 + $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
388 380 } elseif (!empty($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
389 - $active_email_subsection = $_GET['subsection'];
381 + $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
390 382 } else {
391 383 // Default to 'general' only if we're on email section but no specific subsection
392 384 $active_email_subsection = 'general';
393 385 }
@@ -394,16 +386,16 @@
394 386 }
395 387 ?>
396 388 <?php foreach ($section_data['subsections'] as $subsection_id => $subsection_data): ?>
397 389 <?php $is_active_subsection = ($active_email_subsection && $subsection_id === $active_email_subsection); ?>
398 - <a href="?page=easy-invoice-settings&section=email&subsection=<?php echo esc_attr($subsection_id); ?>"
390 + <a href="?page=easy-invoice-settings&section=email&subsection=<?php echo esc_attr($subsection_id); ?>"
399 391 class="email-submenu-link group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-all duration-200 <?php echo $is_active_subsection ? 'text-indigo-700 bg-indigo-50 border-l-2 border-indigo-500' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'; ?>"
400 392 data-subsection="<?php echo esc_attr($subsection_id); ?>"
401 393 data-section="<?php echo esc_attr($section_id); ?>">
402 - <svg class="mr-2 flex-shrink-0 h-4 w-4 <?php echo $is_active_subsection ? 'text-indigo-500' : 'text-gray-400 group-hover:text-gray-500'; ?>" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
394 + <svg class="mr-2 flex-shrink-0 h-4 w-4 <?php echo $is_active_subsection ? 'text-indigo-500' : 'text-gray-400 group-hover:text-indigo-500'; ?>" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
403 395 <polyline points="9,18 15,12 9,6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
404 396 </svg>
405 - <?php
397 + <?php
406 398 // Display shorter names for better UX
407 399 $display_name = '';
408 400 switch ($subsection_id) {
409 401 case 'general':
@@ -433,9 +425,9 @@
433 425 </nav>
434 426 </div>
435 427 </div>
436 428
437 - <!-- Right Content - Settings -->
429 +
438 430 <div class="lg:col-span-2">
439 431 <form id="settings-form" method="post" action="">
440 432 <?php wp_nonce_field('easy_invoice_settings', 'easy_invoice_settings_nonce'); ?>
441 433 <input type="hidden" name="action" value="easy_invoice_save_settings">
@@ -440,27 +432,22 @@
440 432 <?php wp_nonce_field('easy_invoice_settings', 'easy_invoice_settings_nonce'); ?>
441 433 <input type="hidden" name="action" value="easy_invoice_save_settings">
442 434
443 435 <?php foreach ($settings_config as $section_id => $section_data) : ?>
444 - <div id="<?php echo esc_attr($section_id); ?>"
436 + <div id="<?php echo esc_attr($section_id); ?>"
445 437 class="settings-section bg-white shadow rounded-lg <?php echo ($section_id !== $active_section) ? 'hidden' : ''; ?>"
446 - role="tabpanel"
438 + role="tabpanel"
447 439 aria-labelledby="tab-<?php echo esc_attr($section_id); ?>">
448 440 <div class="px-6 py-5 border-b border-gray-200">
449 441 <div class="flex items-center">
450 442 <div class="flex-shrink-0 bg-indigo-100 rounded-lg p-3">
451 - <?php if (isset($section_data['icon']) && !empty($section_data['icon'])): ?>
452 - <?php if (strpos($section_data['icon'], '<svg') === 0): ?>
453 - <?php echo $section_data['icon']; ?>
454 - <?php else: ?>
455 - <i class="<?php echo esc_attr($section_data['icon']); ?> text-indigo-600 text-xl"></i>
456 - <?php endif; ?>
457 - <?php else: ?>
458 - <svg class="text-indigo-600 text-xl h-6 w-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
459 - <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
460 - <path d="M19.4 15A1.65 1.65 0 0 0 18 14A6 6 0 0 0 6 14A1.65 1.65 0 0 0 4.6 15A2 2 0 0 0 6 19H18A2 2 0 0 0 19.4 15Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
461 - </svg>
462 - <?php endif; ?>
443 + <?php
444 + // Content-header icon — render via the same
445 + // helper the section nav uses. Same SVG glyph,
446 + // bigger size (h-6 w-6) and the brand colour
447 + // sits on top of the indigo-100 tile.
448 + easy_invoice_render_settings_section_icon($section_id, 'text-indigo-600', 'h-6 w-6');
449 + ?>
463 450 </div>
464 451 <div class="ml-4">
465 452 <h2 class="text-lg font-medium text-gray-900"><?php echo esc_html($section_data['title']); ?></h2>
466 453 <p class="mt-1 text-sm text-gray-400"><?php echo esc_html($section_data['description']); ?></p>
@@ -473,11 +460,11 @@
473 460 <?php
474 461 // Determine the active subsection from the hash or default to 'general'
475 462 $active_email_subsection = 'general';
476 463 if (isset($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
477 - $active_email_subsection = $_GET['subsection'];
464 + $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
478 465 } elseif (!empty($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
479 - $active_email_subsection = $_GET['subsection'];
466 + $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
480 467 }
481 468 if (!isset($section_data['subsections'][$active_email_subsection])) {
482 469 $active_email_subsection = array_key_first($section_data['subsections']);
483 470 }
@@ -487,9 +474,9 @@
487 474 // Render all email subsections so JS can show/hide them
488 475 foreach ($section_data['subsections'] as $subsection_key => $subsection_data) {
489 476 $is_active = ($subsection_key === $active_email_subsection) ? '' : 'hidden';
490 477 ?>
491 - <div id="email--<?php echo esc_attr($subsection_key); ?>-content" class="email-subsection-content <?php echo $is_active; ?>">
478 + <div id="email--<?php echo esc_attr($subsection_key); ?>-content" class="email-subsection-content <?php echo esc_attr($is_active); ?>">
492 479 <?php
493 480 // Display subsection title
494 481 $subsection_title = '';
495 482 switch ($subsection_key) {
@@ -515,8 +502,55 @@
515 502 <?php if (!empty($subsection_data['description'])): ?>
516 503 <p class="text-sm text-gray-400"><?php echo esc_html($subsection_data['description']); ?></p>
517 504 <?php endif; ?>
518 505 </div>
506 + <?php
507 + // Optional per-subsection notice. Subsection configs may set
508 + // 'notice' => [
509 + // 'type' => 'info' | 'warning' | 'error' | 'success',
510 + // 'title' => string,
511 + // 'message' => string (plain text, may contain links rendered via 'cta_url' + 'cta_label'),
512 + // 'cta_url' => optional URL,
513 + // 'cta_label' => optional CTA button label,
514 + // ]
515 + // Used by PaymentReminderEmail to flag when Smart Reminders
516 + // has taken over so the user understands why the fields below
517 + // are inactive. Renders inline above the fields, not as a
518 + // global admin_notice, so it's anchored to the right section.
519 + if (!empty($subsection_data['notice']) && is_array($subsection_data['notice'])):
520 + $notice = $subsection_data['notice'];
521 + $notice_type = $notice['type'] ?? 'info';
522 + $palette = [
523 + 'info' => ['bg' => 'bg-blue-50', 'border' => 'border-blue-200', 'title' => 'text-blue-900', 'body' => 'text-blue-800', 'icon' => '#1d4ed8'],
524 + 'warning' => ['bg' => 'bg-amber-50', 'border' => 'border-amber-200', 'title' => 'text-amber-900', 'body' => 'text-amber-800', 'icon' => '#b45309'],
525 + 'error' => ['bg' => 'bg-red-50', 'border' => 'border-red-200', 'title' => 'text-red-900', 'body' => 'text-red-800', 'icon' => '#b91c1c'],
526 + 'success' => ['bg' => 'bg-green-50', 'border' => 'border-green-200', 'title' => 'text-green-900', 'body' => 'text-green-800', 'icon' => '#15803d'],
527 + ];
528 + $c = $palette[$notice_type] ?? $palette['info'];
529 + ?>
530 + <div class="rounded-md <?php echo esc_attr($c['bg'] . ' border ' . $c['border']); ?> p-4 mb-6">
531 + <div class="flex">
532 + <div class="flex-shrink-0">
533 + <span class="dashicons dashicons-info-outline" style="color: <?php echo esc_attr($c['icon']); ?>;"></span>
534 + </div>
535 + <div class="ml-3 flex-1">
536 + <?php if (!empty($notice['title'])): ?>
537 + <h4 class="text-sm font-semibold <?php echo esc_attr($c['title']); ?>"><?php echo esc_html($notice['title']); ?></h4>
538 + <?php endif; ?>
539 + <?php if (!empty($notice['message'])): ?>
540 + <p class="mt-1 text-sm <?php echo esc_attr($c['body']); ?>"><?php echo wp_kses_post($notice['message']); ?></p>
541 + <?php endif; ?>
542 + <?php if (!empty($notice['cta_url']) && !empty($notice['cta_label'])): ?>
543 + <p class="mt-3">
544 + <a href="<?php echo esc_url($notice['cta_url']); ?>" class="inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700">
545 + <?php echo esc_html($notice['cta_label']); ?>
546 + </a>
547 + </p>
548 + <?php endif; ?>
549 + </div>
550 + </div>
551 + </div>
552 + <?php endif; ?>
519 553 <div class="space-y-6">
520 554 <?php if (!empty($subsection_data['fields'])) {
521 555 foreach ($subsection_data['fields'] as $option_key => $field_config) {
522 556 $current_value = $settings[$option_key] ?? ($field_config['default'] ?? '');
@@ -530,33 +564,33 @@
530 564 $select_class = 'mt-1 block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md';
531 565 $col_span_class = esc_attr($field_config['col_span'] ?? 'sm:col-span-6');
532 566 echo '<div class="field-wrapper form-group">';
533 567 if ($field_type !== 'checkbox') {
534 - echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700">' . $field_label . '</label>';
568 + echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700">' . esc_html($field_label) . '</label>';
535 569 }
536 -
570 +
537 571 // Define required and aria_describedby variables
538 572 $required = !empty($field_config['required']) ? 'required' : '';
539 573 $aria_describedby = !empty($field_description) ? 'aria-describedby="' . $field_id . '-description"' : '';
540 -
574 +
541 575 switch ($field_type) {
542 576 case 'text':
543 577 case 'email':
544 578 case 'url':
545 579 case 'tel':
546 - echo '<input type="' . esc_attr($field_type) . '" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . $input_class . '" placeholder="' . $field_placeholder . '" ' . $required . ' ' . $aria_describedby . '>';
580 + echo '<input type="' . esc_attr($field_type) . '" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
547 581 break;
548 582 case 'number':
549 583 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
550 584 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
551 585 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
552 - echo '<input type="number" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="' . $input_class . '" placeholder="' . $field_placeholder . '" ' . $required . ' ' . $aria_describedby . '>';
586 + echo '<input type="number" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
553 587 break;
554 588 case 'textarea':
555 - echo '<textarea id="' . $field_id . '" name="' . $field_name . '" rows="3" class="' . $input_class . '" placeholder="' . $field_placeholder . '" ' . $required . ' ' . $aria_describedby . '>' . wp_kses_post($current_value) . '</textarea>';
589 + echo '<textarea id="' . $field_id . '" name="' . esc_html($field_name) . '" rows="3" class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>' . wp_kses_post($current_value) . '</textarea>';
556 590 break;
557 591 case 'select':
558 - echo '<select id="' . $field_id . '" name="' . $field_name . '" class="' . $select_class . '" ' . $required . ' ' . $aria_describedby . '>';
592 + echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '" class="' . esc_html($select_class) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
559 593 if (!empty($field_config['options']) && is_array($field_config['options'])) {
560 594 foreach ($field_config['options'] as $opt_val => $opt_label) {
561 595 echo '<option value="' . esc_attr($opt_val) . '" ' . selected($current_value, $opt_val, false) . '>' . esc_html($opt_label) . '</option>';
562 596 }
@@ -563,25 +597,25 @@
563 597 }
564 598 echo '</select>';
565 599 break;
566 600 case 'multiselect':
567 - echo '<select id="' . $field_id . '" name="' . $field_name . '[]" class="' . $select_class . '" multiple="multiple" aria-label="' . esc_attr($field_label) . '">';
601 + echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '[]" class="' . esc_html($select_class) . '" multiple="multiple" aria-label="' . esc_attr($field_label) . '">';
568 602 if (!empty($field_config['options']) && is_array($field_config['options'])) {
569 603 foreach ($field_config['options'] as $opt_val => $opt_label) {
570 604 $selected = is_array($current_value) && in_array($opt_val, $current_value) ? 'selected="selected"' : '';
571 - echo '<option value="' . esc_attr($opt_val) . '" ' . $selected . '>' . esc_html($opt_label) . '</option>';
605 + echo '<option value="' . esc_attr($opt_val) . '" ' . esc_html($selected) . '>' . esc_html($opt_label) . '</option>';
572 606 }
573 607 }
574 608 echo '</select>';
575 609 break;
576 610 case 'readonly':
577 - echo '<input type="text" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . $input_class . ' bg-gray-50" readonly>';
578 - echo '<input type="hidden" name="' . $field_name . '" value="' . esc_attr($current_value) . '">';
611 + echo '<input type="text" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . esc_html($input_class) . ' bg-gray-50" readonly>';
612 + echo '<input type="hidden" name="' . esc_html($field_name) . '" value="' . esc_attr($current_value) . '">';
579 613 break;
580 614 case 'checkbox':
581 615 echo '<div class="flex items-center">';
582 - echo ' <input type="checkbox" name="' . $field_name . '" id="' . $field_id . '" value="yes" ' . checked($current_value, 'yes', false) . ' class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">';
583 - echo ' <label for="' . $field_id . '" class="ml-2 block text-sm text-gray-900">' . $field_label . '</label>';
616 + echo ' <input type="checkbox" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="yes" ' . checked($current_value, 'yes', false) . ' class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">';
617 + echo ' <label for="' . $field_id . '" class="ml-2 block text-sm text-gray-900">' . esc_html($field_label) . '</label>';
584 618 echo '</div>';
585 619 if (!empty($field_description)) {
586 620 echo '<p id="' . $field_id . '-description" class="mt-2 text-sm text-gray-400 leading-relaxed">' . esc_html($field_description) . '</p>';
587 621 }
@@ -588,14 +622,14 @@
588 622 break;
589 623 case 'image':
590 624 echo '<div class="mt-1 flex items-center">';
591 625 echo ' <span class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100">';
592 - echo ' <img id="' . $field_id . '-preview" src="' . esc_url($current_value) . '" alt="' . esc_attr($field_label) . '" class="h-full w-full object-cover' . (empty($current_value) ? ' hidden' : '') . '">';
626 + echo ' <img id="' . $field_id . '-preview"' . (empty($current_value) ? '' : ' src="' . esc_url($current_value) . '"') . ' alt="' . esc_attr($field_label) . '" class="h-full w-full object-cover' . (empty($current_value) ? ' hidden' : '') . '">';
593 627 echo ' </span>';
594 628 echo ' <button type="button" id="upload_image_button_' . $field_id . '" class="upload-logo-button ml-5 bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" data-uploader_title="' . esc_attr__('Choose Logo', 'easy-invoice') . '" data-uploader_button_text="' . esc_attr__('Select Logo', 'easy-invoice') . '">';
595 629 echo esc_html__('Change', 'easy-invoice');
596 630 echo '</button>';
597 - echo ' <input type="hidden" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
631 + echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
598 632 echo '</div>';
599 633 break;
600 634 case 'wp_editor':
601 635 wp_editor( $current_value, $field_id, ['textarea_name' => $field_name, 'teeny' => true, 'media_buttons' => false, 'textarea_rows' => 7, 'editor_class' => 'mt-1'] );
@@ -600,9 +634,9 @@
600 634 case 'wp_editor':
601 635 wp_editor( $current_value, $field_id, ['textarea_name' => $field_name, 'teeny' => true, 'media_buttons' => false, 'textarea_rows' => 7, 'editor_class' => 'mt-1'] );
602 636 // Handle description for wp_editor specifically
603 637 if (!empty($field_description)) {
604 - echo '<p class="mt-1 text-sm text-gray-400">' . esc_html($field_description) . '</p>';
638 + echo '<p id="' . $field_id . '-description" class="mt-1 text-sm text-gray-400">' . wp_kses($field_description, ['a' => ['href' => [], 'target' => [], 'class' => []], 'code' => [], 'strong' => [], 'em' => []]) . '</p>';
605 639 }
606 640 break;
607 641 case 'test_email':
608 642 echo '<div class="mt-1">';
@@ -621,10 +655,10 @@
621 655 $is_payment_test = ($field_id === 'easy_invoice_payment_test_template');
622 656 $width_class = $is_payment_test ? 'w-[200px]' : 'w-64';
623 657 echo '<div class="mt-1">';
624 658 echo ' <div class="flex items-center space-x-3">';
625 - echo ' <input type="email" id="' . $field_id . '_email" placeholder="' . esc_attr__('Enter email address to test', 'easy-invoice') . '" class="mt-1 block ' . $width_class . ' border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" value="' . esc_attr(get_option('admin_email')) . '">';
626 - echo ' <button type="button" id="' . $field_id . '_button" class="test-template-email-button inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 h-[38px]" data-template-type="' . esc_attr($template_type) . '">';
659 + echo ' <input type="email" id="' . $field_id . '_email" placeholder="' . esc_attr__('Enter email address to test', 'easy-invoice') . '" class="mt-1 block ' . esc_html($width_class) . ' border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" value="' . esc_attr(get_option('admin_email')) . '">';
660 + echo ' <button type="button" id="' . $field_id . '_button" data-template-type="' . esc_attr($field_config['template_type'] ?? '') . '" class="test-template-email-button inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 h-[38px]" data-template-type="' . esc_attr($template_type) . '">';
627 661 echo ' <i class="fas fa-envelope mr-2" aria-hidden="true"></i>';
628 662 echo ' ' . esc_html__('Test Template', 'easy-invoice');
629 663 echo ' </button>';
630 664 echo ' </div>';
@@ -643,20 +677,20 @@
643 677 echo ' <div id="' . $field_id . '_result" class="mt-2" style="display: none;"></div>';
644 678 echo '</div>';
645 679 break;
646 680 default:
647 - echo '<input type="text" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . $input_class . '" placeholder="' . $field_placeholder . '">';
681 + echo '<input type="text" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '">';
648 682 break;
649 683 }
650 684 if ($field_type !== 'checkbox' && $field_type !== 'wp_editor' && !empty($field_description)) {
651 - echo '<p class="mt-1 text-sm text-gray-400">' . esc_html($field_description) . '</p>';
685 + echo '<p id="' . $field_id . '-description" class="mt-1 text-sm text-gray-400">' . wp_kses($field_description, ['a' => ['href' => [], 'target' => [], 'class' => []], 'code' => [], 'strong' => [], 'em' => []]) . '</p>';
652 686 }
653 687 echo '</div>'; // Close the field wrapper div
654 688 }
655 689 }
656 690 ?>
657 -
658 - <?php
691 +
692 + <?php
659 693 // Show placeholders only for specific email subsections (not General)
660 694 if (in_array($subsection_key, ['invoice_available', 'quote_available', 'payment_received', 'payment_reminder'])): ?>
661 695 <div class="mt-6">
662 696 <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
@@ -662,26 +696,31 @@
662 696 <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
663 697 <p class="text-sm font-medium text-blue-900 mb-3"><?php esc_html_e('Available Placeholders:', 'easy-invoice'); ?></p>
664 698 <div class="grid grid-cols-2 md:grid-cols-3 gap-2">
665 699 <?php if (in_array($subsection_key, ['invoice_available', 'payment_received', 'payment_reminder'])): ?>
666 - <!-- Invoice-specific placeholders -->
700 +
667 701 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{invoice_number}}</div>
668 702 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{due_date}}</div>
669 703 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{invoice_url}}</div>
670 704 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{payment_url}}</div>
671 705 <?php endif; ?>
672 -
706 +
673 707 <?php if (in_array($subsection_key, ['quote_available'])): ?>
674 - <!-- Quote-specific placeholders -->
708 +
675 709 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{quote_number}}</div>
676 710 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{expiry_date}}</div>
677 711 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{quote_url}}</div>
678 712 <?php endif; ?>
713 +
679 714
680 - <!-- Common placeholders for all templates -->
681 715 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_name}}</div>
716 + <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_first_name}}</div>
717 + <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_last_name}}</div>
718 + <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_email}}</div>
719 + <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_address}}</div>
682 720 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{company_name}}</div>
683 721 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{total_amount}}</div>
722 + <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{amount_due}}</div>
684 723 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{issue_date}}</div>
685 724 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{company_email}}</div>
686 725 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{company_phone}}</div>
687 726 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{payment_terms}}</div>
@@ -699,23 +738,153 @@
699 738 <div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
700 739 <div class="flex items-center">
701 740 <i class="fas fa-info-circle text-blue-500 mr-2"></i>
702 741 <p class="text-sm text-blue-700">
703 - <?php _e('Drag and drop payment methods to reorder them. The order will be reflected on payment forms.', 'easy-invoice'); ?>
742 + <?php esc_html_e('Drag and drop payment methods to reorder them. The order will be reflected on payment forms.', 'easy-invoice'); ?>
704 743 </p>
705 744 </div>
706 745 </div>
707 746 <?php endif; ?>
708 -
747 +
748 + <?php
749 + /**
750 + * Pro-gateway teaser block (renders only when Pro is NOT active).
751 + *
752 + * Showing the names of Pro gateways here drives conversion — users
753 + * see exactly what they unlock without leaving the settings page.
754 + * Each row links to the pricing page in a new tab. Free gateways
755 + * (PayPal, Manual) remain enabled below this block as usual.
756 + */
757 + if (!easy_invoice_has_pro()) :
758 + $pro_gateway_teasers = [
759 + 'stripe' => [
760 + 'title' => __('Stripe', 'easy-invoice'),
761 + 'desc' => __('Cards, Apple Pay, Google Pay, Link. SCA / 3-D Secure built in.', 'easy-invoice'),
762 + ],
763 + 'square' => [
764 + 'title' => __('Square', 'easy-invoice'),
765 + 'desc' => __('US / CA / UK / AU / JP card processing with webhook reconciliation.', 'easy-invoice'),
766 + ],
767 + 'authorizenet' => [
768 + 'title' => __('Authorize.Net', 'easy-invoice'),
769 + 'desc' => __('Long-standing US gateway — credit cards + eChecks via AIM.', 'easy-invoice'),
770 + ],
771 + 'mollie' => [
772 + 'title' => __('Mollie', 'easy-invoice'),
773 + 'desc' => __('Europe-friendly: SEPA, iDEAL, Bancontact, Sofort, Klarna, card.', 'easy-invoice'),
774 + ],
775 + 'paystack' => [
776 + 'title' => __('Paystack', 'easy-invoice'),
777 + 'desc' => __('Africa-first (NGN / GHS / ZAR / KES / USD): card, bank, USSD, mobile money, QR.', 'easy-invoice'),
778 + ],
779 + 'moneris' => [
780 + 'title' => __('Moneris', 'easy-invoice'),
781 + 'desc' => __('Canada\'s largest card processor — Visa, Mastercard, AMEX, Interac.', 'easy-invoice'),
782 + ],
783 + 'bank_transfer'=> [
784 + 'title' => __('Bank Transfer', 'easy-invoice'),
785 + 'desc' => __('Show your IBAN / SWIFT on invoices; mark paid on wire arrival.', 'easy-invoice'),
786 + ],
787 + 'cheque' => [
788 + 'title' => __('Cheque', 'easy-invoice'),
789 + 'desc' => __('Display your mailing address; mark paid when the cheque clears.', 'easy-invoice'),
790 + ],
791 + 'cash' => [
792 + 'title' => __('Cash', 'easy-invoice'),
793 + 'desc' => __('In-person cash with custom collection instructions.', 'easy-invoice'),
794 + ],
795 + ];
796 +
797 + // Hide the ones we already register in Free / are already shown above.
798 + $already_registered = array_keys($all_gateways_sorted);
799 + foreach ($already_registered as $id) {
800 + unset($pro_gateway_teasers[$id]);
801 + }
802 + ?>
803 + <?php if (!empty($pro_gateway_teasers)): ?>
804 + <div class="ei-pro-gateways mt-2 mb-6 rounded-xl border border-gray-200 bg-white shadow-sm overflow-hidden">
805 +
806 + <div class="flex items-center justify-between px-5 py-4 border-b border-gray-100 bg-gradient-to-r from-indigo-50/60 to-transparent">
807 + <div class="flex items-center min-w-0">
808 + <span class="inline-flex items-center justify-center w-9 h-9 rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 text-white shadow-sm mr-3 flex-shrink-0">
809 + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
810 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
811 + </svg>
812 + </span>
813 + <div class="min-w-0">
814 + <h3 class="text-sm font-semibold text-gray-900 truncate">
815 + <?php
816 + printf(
817 + /* translators: %d is the number of additional gateways */
818 + esc_html__('Unlock %d more payment gateways with Easy Invoice Pro', 'easy-invoice'),
819 + count($pro_gateway_teasers)
820 + );
821 + ?>
822 + </h3>
823 + <p class="mt-0.5 text-xs text-gray-500"><?php esc_html_e('Stripe, Square, Mollie, Paystack and more — installed alongside PayPal and Manual.', 'easy-invoice'); ?></p>
824 + </div>
825 + </div>
826 + <a href="https://matrixaddons.com/plugins/easy-invoice/#pricing" target="_blank" rel="noopener"
827 + class="ml-4 shrink-0 inline-flex items-center px-3 py-1.5 rounded-md text-xs font-semibold text-white bg-indigo-600 hover:bg-indigo-700 transition-colors">
828 + <?php esc_html_e('View pricing', 'easy-invoice'); ?>
829 + <svg class="w-3 h-3 ml-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
830 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 5l7 7-7 7"/>
831 + </svg>
832 + </a>
833 + </div>
834 +
835 +
836 + <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 divide-y sm:divide-y-0 sm:divide-x divide-gray-100 lg:divide-x">
837 + <?php
838 + $teaser_idx = 0;
839 + $teaser_count = count($pro_gateway_teasers);
840 + foreach ($pro_gateway_teasers as $teaser_id => $teaser):
841 + $teaser_idx++;
842 + // Mark items that start a new row so the row-divider rules look correct on every grid breakpoint.
843 + $row_break_sm = ($teaser_idx > 2); // 2 cols
844 + $row_break_lg = ($teaser_idx > 3); // 3 cols
845 + $initial = strtoupper(mb_substr($teaser['title'], 0, 1));
846 + ?>
847 + <a href="https://matrixaddons.com/plugins/easy-invoice/#pricing" target="_blank" rel="noopener"
848 + class="ei-pro-gateway-card group relative flex items-center gap-3 px-4 py-3.5 hover:bg-indigo-50/40 transition-colors <?php echo $row_break_sm ? 'sm:border-t sm:border-gray-100' : ''; ?> <?php echo $row_break_lg ? 'lg:border-t lg:border-gray-100' : ''; ?>"
849 + data-gateway-id="<?php echo esc_attr($teaser_id); ?>">
850 +
851 + <span class="flex-shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-md bg-gradient-to-br from-gray-50 to-gray-100 border border-gray-200 text-sm font-bold text-gray-700 group-hover:from-indigo-50 group-hover:to-indigo-100 group-hover:border-indigo-200 group-hover:text-indigo-700 transition-all">
852 + <?php echo esc_html($initial); ?>
853 + </span>
854 + <div class="min-w-0 flex-1">
855 + <div class="text-sm font-semibold text-gray-900 truncate"><?php echo esc_html($teaser['title']); ?></div>
856 + <p class="mt-0.5 text-xs text-gray-500 leading-snug line-clamp-2"><?php echo esc_html($teaser['desc']); ?></p>
857 + </div>
858 + <svg class="w-4 h-4 text-gray-300 group-hover:text-indigo-500 group-hover:translate-x-0.5 flex-shrink-0 transition-all" fill="none" stroke="currentColor" viewBox="0 0 24 24">
859 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
860 + </svg>
861 + </a>
862 + <?php endforeach; ?>
863 + </div>
864 + </div>
865 + <?php endif; ?>
866 + <?php endif; ?>
867 +
868 + <div id="ei-offline-add" class="mb-5 rounded-lg border border-dashed border-gray-300 bg-white p-4">
869 + <div class="flex flex-col sm:flex-row sm:items-end gap-3">
870 + <div class="flex-1">
871 + <label for="ei-offline-add-title" class="block text-sm font-medium text-gray-900"><?php esc_html_e('Add an offline payment method', 'easy-invoice'); ?></label>
872 + <p class="mt-0.5 text-xs text-gray-500"><?php esc_html_e('For money you receive outside a card gateway — ACH, wire transfer, mobile money, a second bank account. Each method gets its own instructions, an optional reference field and a receipt upload; clients pick it on the invoice and you confirm the payment on the Payments screen.', 'easy-invoice'); ?></p>
873 + <input type="text" id="ei-offline-add-title" maxlength="60" placeholder="<?php esc_attr_e('e.g. ACH, Wire transfer', 'easy-invoice'); ?>" class="mt-2 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 text-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
874 + </div>
875 + <button type="button" id="ei-offline-add-btn" class="inline-flex items-center justify-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"><?php esc_html_e('Add method', 'easy-invoice'); ?></button>
876 + </div>
877 + </div>
709 878 <div id="sortable-gateways" class="space-y-5">
710 879 <?php foreach ($all_gateways_sorted as $gateway_id => $gateway) : ?>
711 880 <div class="gateway-item bg-gray-50 p-5 rounded-lg border border-gray-200 cursor-move" data-gateway-id="<?php echo esc_attr($gateway_id); ?>">
712 881 <div class="flex items-center mb-3 space-x-3">
713 - <!-- Drag Handle -->
882 +
714 883 <div class="gateway-handle text-gray-400 hover:text-gray-600 cursor-move mr-2">
715 884 <i class="fas fa-grip-vertical"></i>
716 885 </div>
717 -
886 +
718 887 <input type="checkbox"
719 888 class="gateway-enable-checkbox h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
720 889 name="settings[easy_invoice_payment_methods][]"
721 890 id="gateway-enable-<?php echo esc_attr($gateway_id); ?>"
@@ -720,26 +889,26 @@
720 889 name="settings[easy_invoice_payment_methods][]"
721 890 id="gateway-enable-<?php echo esc_attr($gateway_id); ?>"
722 891 value="<?php echo esc_attr($gateway_id); ?>"
723 892 <?php checked(!empty($payment_methods_enabled) && in_array($gateway_id, $payment_methods_enabled)); ?>>
724 -
893 +
725 894 <div class="flex-1">
726 895 <label for="gateway-enable-<?php echo esc_attr($gateway_id); ?>" class="font-medium text-gray-900">
727 896 <?php echo esc_html(method_exists($gateway, 'getTitle') ? $gateway->getTitle() : $gateway_id); ?>
728 897 </label>
729 898 <?php if (method_exists($gateway, 'getDescription') && $gateway->getDescription()): ?>
730 - <span class="block text-sm text-gray-400"><?php echo esc_html($gateway->getDescription()); ?></span>
899 + <span class="block text-sm text-gray-400"><?php echo wp_kses_post($gateway->getDescription()); ?></span>
731 900 <?php endif; ?>
732 901 </div>
733 902 </div>
903 +
734 904
735 - <!-- Custom Display Name Field -->
736 905 <div class="gateway-settings <?php echo empty($payment_methods_enabled) || !in_array($gateway_id, $payment_methods_enabled) ? 'hidden' : ''; ?>">
737 906 <div class="mb-4">
738 907 <label for="gateway-display-name-<?php echo esc_attr($gateway_id); ?>" class="block text-sm font-medium text-gray-700">
739 - <?php _e('Display Name', 'easy-invoice'); ?>
908 + <?php esc_html_e('Display Name', 'easy-invoice'); ?>
740 909 </label>
741 - <input type="text"
910 + <input type="text"
742 911 id="gateway-display-name-<?php echo esc_attr($gateway_id); ?>"
743 912 name="settings[easy_invoice_gateway_display_name_<?php echo esc_attr($gateway_id); ?>]"
744 913 value="<?php echo esc_attr($settings['easy_invoice_gateway_display_name_' . $gateway_id] ?? (method_exists($gateway, 'getTitle') ? $gateway->getTitle() : ucfirst(str_replace('_', ' ', $gateway_id)))); ?>"
745 914 placeholder="<?php echo esc_attr(method_exists($gateway, 'getTitle') ? $gateway->getTitle() : ucfirst(str_replace('_', ' ', $gateway_id))); ?>"
@@ -744,13 +913,13 @@
744 913 value="<?php echo esc_attr($settings['easy_invoice_gateway_display_name_' . $gateway_id] ?? (method_exists($gateway, 'getTitle') ? $gateway->getTitle() : ucfirst(str_replace('_', ' ', $gateway_id)))); ?>"
745 914 placeholder="<?php echo esc_attr(method_exists($gateway, 'getTitle') ? $gateway->getTitle() : ucfirst(str_replace('_', ' ', $gateway_id))); ?>"
746 915 class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
747 916 <p class="mt-1 text-xs text-gray-500">
748 - <?php _e('Leave blank to use the default name. This name will appear on invoices and payment forms.', 'easy-invoice'); ?>
917 + <?php esc_html_e('Leave blank to use the default name. This name will appear on invoices and payment forms.', 'easy-invoice'); ?>
749 918 </p>
750 919 </div>
751 -
752 - <?php
920 +
921 + <?php
753 922 $gateway_config = method_exists($gateway, 'getSettingsConfig') ? $gateway->getSettingsConfig() : [];
754 923 if (!empty($gateway_config['fields'])): ?>
755 924 <div class="grid grid-cols-1 md:grid-cols-6 gap-4">
756 925 <?php foreach ($gateway_config['fields'] as $option_key => $field_config): ?>
@@ -761,10 +930,13 @@
761 930 <?php endforeach; ?>
762 931 </div>
763 932 <?php endif; ?>
764 933 </div>
765 -
934 +
766 935 <input type="hidden" class="gateway-order-input" name="settings[easy_invoice_gateway_order][]" value="<?php echo esc_attr($gateway_id); ?>">
936 + <?php if ($gateway instanceof \EasyInvoice\Gateways\OfflineGateway && $gateway->isCustom()) : ?>
937 + <p class="mt-3 text-xs text-gray-500"><?php esc_html_e('Added by you.', 'easy-invoice'); ?> <button type="button" class="ei-offline-remove font-medium text-red-600 hover:text-red-800" data-method-id="<?php echo esc_attr($gateway_id); ?>" data-method-title="<?php echo esc_attr($gateway->getTitle()); ?>"><?php esc_html_e('Remove this method', 'easy-invoice'); ?></button> <span class="text-gray-400">· <?php esc_html_e('Payments already recorded keep it.', 'easy-invoice'); ?></span></p>
938 + <?php endif; ?>
767 939 </div>
768 940 <?php endforeach; ?>
769 941 </div>
770 942 </div>
@@ -828,18 +1000,18 @@
828 1000 case 'text':
829 1001 case 'email':
830 1002 case 'url':
831 1003 case 'tel':
832 - echo '<input type="' . esc_attr($field_config['type']) . '" name="settings[' . esc_attr($tax_key) . ']" id="' . esc_attr($tax_key) . '" value="' . esc_attr($current_value) . '" class="' . $input_class . '" placeholder="' . esc_attr($field_config['placeholder'] ?? '') . '">';
1004 + echo '<input type="' . esc_attr($field_config['type']) . '" name="settings[' . esc_attr($tax_key) . ']" id="' . esc_attr($tax_key) . '" value="' . esc_attr($current_value) . '" class="' . esc_html($input_class) . '" placeholder="' . esc_attr($field_config['placeholder'] ?? '') . '">';
833 1005 break;
834 1006 case 'number':
835 1007 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
836 1008 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
837 1009 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
838 - echo '<input type="number" name="settings[' . esc_attr($tax_key) . ']" id="' . esc_attr($tax_key) . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="' . $input_class . '" placeholder="' . esc_attr($field_config['placeholder'] ?? '') . '">';
1010 + echo '<input type="number" name="settings[' . esc_attr($tax_key) . ']" id="' . esc_attr($tax_key) . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="' . esc_html($input_class) . '" placeholder="' . esc_attr($field_config['placeholder'] ?? '') . '">';
839 1011 break;
840 1012 case 'select':
841 - echo '<select id="' . esc_attr($tax_key) . '" name="settings[' . esc_attr($tax_key) . ']" class="' . $select_class . '">';
1013 + echo '<select id="' . esc_attr($tax_key) . '" name="settings[' . esc_attr($tax_key) . ']" class="' . esc_html($select_class) . '">';
842 1014 if (!empty($field_config['options']) && is_array($field_config['options'])) {
843 1015 foreach ($field_config['options'] as $opt_val => $opt_label) {
844 1016 echo '<option value="' . esc_attr($opt_val) . '" ' . selected($current_value, $opt_val, false) . '>' . esc_html($opt_label) . '</option>';
845 1017 }
@@ -852,17 +1024,18 @@
852 1024 }
853 1025 echo '</div>';
854 1026 }
855 1027 echo '</div>';
856 -
1028 +
857 1029 // Allow additional fields to be added to the tax section
858 1030 $additional_tax_fields = apply_filters('easy_invoice_tax_section_additional_fields', [], $settings);
859 1031 if (!empty($additional_tax_fields)) {
860 1032 echo '<div class="mt-6 pt-6 border-t border-gray-200">';
861 - echo '<h4 class="text-sm font-medium text-gray-900 mb-4">' . __('Additional Tax Settings', 'easy-invoice-pro') . '</h4>';
1033 + echo '<h4 class="text-sm font-medium text-gray-900 mb-4">' . esc_html__('Additional Tax Settings', 'easy-invoice') . '</h4>';
862 1034 echo '<div class="grid grid-cols-1 md:grid-cols-2 gap-6">';
863 1035 foreach ($additional_tax_fields as $field_key => $field_config) {
864 - $current_value = $settings[$field_key] ?? ($field_config['default'] ?? '');
1036 + // Filter-only fields are not in the loaded settings array; read the option itself.
1037 + $current_value = array_key_exists($field_key, (array) $settings) ? $settings[$field_key] : get_option($field_key, $field_config['default'] ?? '');
865 1038 easy_invoice_render_field($field_key, $field_config, $current_value);
866 1039 }
867 1040 echo '</div>';
868 1041 echo '</div>';
@@ -900,9 +1073,9 @@
900 1073 $form_group_class = $col_span_class;
901 1074 } else {
902 1075 $form_group_class = $col_span_class;
903 1076 }
904 -
1077 +
905 1078 // Handle conditional display
906 1079 $conditional_class = '';
907 1080 if (isset($field_config['depends_on']) && is_array($field_config['depends_on'])) {
908 1081 $conditional_class = ' conditional-field';
@@ -910,36 +1083,36 @@
910 1083 $depends_field_name = 'settings[' . esc_attr($depends_key) . ']';
911 1084 $conditional_class .= ' depends-on-' . esc_attr($depends_key) . '-' . esc_attr($depends_value);
912 1085 }
913 1086 }
914 -
915 - echo '<div class="form-group ' . $form_group_class . $conditional_class . '">';
1087 +
1088 + echo '<div class="form-group ' . esc_html($form_group_class) . esc_html($conditional_class) . '">';
916 1089 if ($field_type !== 'checkbox') {
917 - echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700">' . $field_label . '</label>';
1090 + echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700">' . esc_html($field_label) . '</label>';
918 1091 }
919 -
1092 +
920 1093 // Define required and aria_describedby variables
921 1094 $required = !empty($field_config['required']) ? 'required' : '';
922 1095 $aria_describedby = !empty($field_description) ? 'aria-describedby="' . $field_id . '-description"' : '';
923 -
1096 +
924 1097 switch ($field_type) {
925 1098 case 'text':
926 1099 case 'email':
927 1100 case 'url':
928 1101 case 'tel':
929 - echo '<input type="' . esc_attr($field_type) . '" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . $input_class . '" placeholder="' . $field_placeholder . '" ' . $required . ' ' . $aria_describedby . '>';
1102 + echo '<input type="' . esc_attr($field_type) . '" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
930 1103 break;
931 1104 case 'number':
932 1105 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
933 1106 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
934 1107 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
935 - echo '<input type="number" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="' . $input_class . '" placeholder="' . $field_placeholder . '" ' . $required . ' ' . $aria_describedby . '>';
1108 + echo '<input type="number" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
936 1109 break;
937 1110 case 'textarea':
938 - echo '<textarea id="' . $field_id . '" name="' . $field_name . '" rows="3" class="' . $input_class . '" placeholder="' . $field_placeholder . '" ' . $required . ' ' . $aria_describedby . '>' . wp_kses_post($current_value) . '</textarea>';
1111 + echo '<textarea id="' . $field_id . '" name="' . esc_html($field_name) . '" rows="3" class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>' . wp_kses_post($current_value) . '</textarea>';
939 1112 break;
940 1113 case 'select':
941 - echo '<select id="' . $field_id . '" name="' . $field_name . '" class="' . $select_class . '" ' . $required . ' ' . $aria_describedby . '>';
1114 + echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '" class="' . esc_html($select_class) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
942 1115 if (!empty($field_config['options']) && is_array($field_config['options'])) {
943 1116 foreach ($field_config['options'] as $opt_val => $opt_label) {
944 1117 echo '<option value="' . esc_attr($opt_val) . '" ' . selected(strtolower($current_value), strtolower($opt_val), false) . '>' . esc_html($opt_label) . '</option>';
945 1118 }
@@ -946,25 +1119,25 @@
946 1119 }
947 1120 echo '</select>';
948 1121 break;
949 1122 case 'multiselect':
950 - echo '<select id="' . $field_id . '" name="' . $field_name . '[]" class="' . $select_class . '" multiple="multiple" aria-label="' . esc_attr($field_label) . '">';
1123 + echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '[]" class="' . esc_html($select_class) . '" multiple="multiple" aria-label="' . esc_attr($field_label) . '">';
951 1124 if (!empty($field_config['options']) && is_array($field_config['options'])) {
952 1125 foreach ($field_config['options'] as $opt_val => $opt_label) {
953 1126 $selected = is_array($current_value) && in_array($opt_val, $current_value) ? 'selected="selected"' : '';
954 - echo '<option value="' . esc_attr($opt_val) . '" ' . $selected . '>' . esc_html($opt_label) . '</option>';
1127 + echo '<option value="' . esc_attr($opt_val) . '" ' . esc_html($selected) . '>' . esc_html($opt_label) . '</option>';
955 1128 }
956 1129 }
957 1130 echo '</select>';
958 1131 break;
959 1132 case 'readonly':
960 - echo '<input type="text" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . $input_class . ' bg-gray-50" readonly>';
961 - echo '<input type="hidden" name="' . $field_name . '" value="' . esc_attr($current_value) . '">';
1133 + echo '<input type="text" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . esc_html($input_class) . ' bg-gray-50" readonly>';
1134 + echo '<input type="hidden" name="' . esc_html($field_name) . '" value="' . esc_attr($current_value) . '">';
962 1135 break;
963 1136 case 'checkbox':
964 1137 echo '<div class="flex items-center">';
965 - echo ' <input type="checkbox" name="' . $field_name . '" id="' . $field_id . '" value="yes" ' . checked($current_value, 'yes', false) . ' class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">';
966 - echo ' <label for="' . $field_id . '" class="ml-2 block text-sm text-gray-900">' . $field_label . '</label>';
1138 + echo ' <input type="checkbox" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="yes" ' . checked($current_value, 'yes', false) . ' class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">';
1139 + echo ' <label for="' . $field_id . '" class="ml-2 block text-sm text-gray-900">' . esc_html($field_label) . '</label>';
967 1140 echo '</div>';
968 1141 if (!empty($field_description)) {
969 1142 echo '<p id="' . $field_id . '-description" class="mt-2 text-sm text-gray-400 leading-relaxed">' . esc_html($field_description) . '</p>';
970 1143 }
@@ -971,14 +1144,14 @@
971 1144 break;
972 1145 case 'image':
973 1146 echo '<div class="mt-1 flex items-center">';
974 1147 echo ' <span class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100">';
975 - echo ' <img id="' . $field_id . '-preview" src="' . esc_url($current_value) . '" alt="' . esc_attr($field_label) . '" class="h-full w-full object-cover' . (empty($current_value) ? ' hidden' : '') . '">';
1148 + echo ' <img id="' . $field_id . '-preview"' . (empty($current_value) ? '' : ' src="' . esc_url($current_value) . '"') . ' alt="' . esc_attr($field_label) . '" class="h-full w-full object-cover' . (empty($current_value) ? ' hidden' : '') . '">';
976 1149 echo ' </span>';
977 1150 echo ' <button type="button" id="upload_image_button_' . $field_id . '" class="upload-logo-button ml-5 bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" data-uploader_title="' . esc_attr__('Choose Logo', 'easy-invoice') . '" data-uploader_button_text="' . esc_attr__('Select Logo', 'easy-invoice') . '">';
978 1151 echo esc_html__('Change', 'easy-invoice');
979 1152 echo '</button>';
980 - echo ' <input type="hidden" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
1153 + echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
981 1154 echo '</div>';
982 1155 break;
983 1156 case 'wp_editor':
984 1157 wp_editor( $current_value, $field_id, ['textarea_name' => $field_name, 'teeny' => true, 'media_buttons' => false, 'textarea_rows' => 7, 'editor_class' => 'mt-1'] );
@@ -983,25 +1156,25 @@
983 1156 case 'wp_editor':
984 1157 wp_editor( $current_value, $field_id, ['textarea_name' => $field_name, 'teeny' => true, 'media_buttons' => false, 'textarea_rows' => 7, 'editor_class' => 'mt-1'] );
985 1158 // Handle description for wp_editor specifically
986 1159 if (!empty($field_description)) {
987 - echo '<p class="mt-1 text-sm text-gray-400">' . esc_html($field_description) . '</p>';
1160 + echo '<p id="' . $field_id . '-description" class="mt-1 text-sm text-gray-400">' . wp_kses($field_description, ['a' => ['href' => [], 'target' => [], 'class' => []], 'code' => [], 'strong' => [], 'em' => []]) . '</p>';
988 1161 }
989 1162 break;
990 1163 case 'color':
991 - echo '<input type="color" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="h-10 w-20 border border-gray-300 rounded">';
1164 + echo '<input type="color" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="h-10 w-20 border border-gray-300 rounded">';
992 1165 break;
993 1166 case 'range':
994 1167 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
995 1168 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
996 1169 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
997 - echo '<input type="range" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="w-full">';
1170 + echo '<input type="range" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="w-full">';
998 1171 echo '<span class="text-sm text-gray-400">' . esc_html($current_value) . '%</span>';
999 1172 break;
1000 1173 case 'image_upload':
1001 1174 echo '<div class="image-upload-wrap">';
1002 1175 echo '<div class="flex items-center space-x-3">';
1003 - echo ' <input type="hidden" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
1176 + echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
1004 1177 echo ' <button type="button" class="upload-image-button bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" data-target="' . $field_id . '">';
1005 1178 echo esc_html__('Upload Image', 'easy-invoice');
1006 1179 echo '</button>';
1007 1180 echo '</div>';
@@ -1018,18 +1191,18 @@
1018 1191 $button_class_name = 'regenerate-invoice-numbers-button';
1019 1192 if ($option_key === 'easy_invoice_regenerate_quote_numbers') {
1020 1193 $button_class_name = 'regenerate-quote-numbers-button';
1021 1194 }
1022 - echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' ' . $button_class_name . '">';
1195 + echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' ' . wp_kses_post($button_class_name) . '">';
1023 1196 echo esc_html($button_text);
1024 1197 echo '</button>';
1025 1198 break;
1026 1199 default:
1027 - echo '<input type="text" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . $input_class . '" placeholder="' . $field_placeholder . '">';
1200 + echo '<input type="text" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . esc_html($input_class) . '" placeholder="' . esc_html($field_placeholder) . '">';
1028 1201 break;
1029 1202 }
1030 1203 if ($field_type !== 'checkbox' && $field_type !== 'wp_editor' && !empty($field_description)) {
1031 - echo '<p class="mt-1 text-sm text-gray-400">' . esc_html($field_description) . '</p>';
1204 + echo '<p id="' . $field_id . '-description" class="mt-1 text-sm text-gray-400">' . wp_kses($field_description, ['a' => ['href' => [], 'target' => [], 'class' => []], 'code' => [], 'strong' => [], 'em' => []]) . '</p>';
1032 1205 }
1033 1206 echo '</div>';
1034 1207 }
1035 1208 if ($is_grid) echo '</div>';
@@ -1044,10 +1217,10 @@
1044 1217 </div>
1045 1218 </div>
1046 1219 </div>
1047 1220 <?php endforeach; ?>
1221 +
1048 1222
1049 - <!-- Bottom Save Button -->
1050 1223 <div class="mt-8 pt-6 border-t border-gray-200">
1051 1224 <div class="flex items-center justify-start space-x-3">
1052 1225 <button type="button" id="save-settings-bottom" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200" aria-label="<?php esc_attr_e('Save Settings', 'easy-invoice'); ?>">
1053 1226 <i class="fas fa-save mr-2" aria-hidden="true"></i>
@@ -1054,9 +1227,9 @@
1054 1227 <?php esc_html_e('Save Changes', 'easy-invoice'); ?>
1055 1228 </button>
1056 1229 </div>
1057 1230 </div>
1058 -
1231 +
1059 1232 </form>
1060 1233 </div>
1061 1234 </div>
1062 1235 </div>
@@ -1066,14 +1239,14 @@
1066 1239 document.addEventListener('DOMContentLoaded', function() {
1067 1240 // Enter key functionality to save settings
1068 1241 const settingsForm = document.getElementById('settings-form');
1069 1242 const saveButton = document.getElementById('save-settings-bottom');
1070 -
1243 +
1071 1244 if (settingsForm && saveButton) {
1072 1245 // Get both save buttons
1073 1246 const topSaveButton = document.getElementById('save-settings');
1074 1247 const bottomSaveButton = document.getElementById('save-settings-bottom');
1075 -
1248 +
1076 1249 // Function to trigger save
1077 1250 function triggerSave() {
1078 1251 // Try bottom button first, then top button
1079 1252 if (bottomSaveButton) {
@@ -1081,9 +1254,9 @@
1081 1254 } else if (topSaveButton) {
1082 1255 topSaveButton.click();
1083 1256 }
1084 1257 }
1085 -
1258 +
1086 1259 // Listen for Enter key on form inputs
1087 1260 settingsForm.addEventListener('keydown', function(e) {
1088 1261 // Check if Enter key is pressed and not in a textarea or select
1089 1262 if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA' && e.target.tagName !== 'SELECT') {
@@ -1090,9 +1263,9 @@
1090 1263 e.preventDefault();
1091 1264 triggerSave();
1092 1265 }
1093 1266 });
1094 -
1267 +
1095 1268 // Also listen for Enter key on the entire form
1096 1269 settingsForm.addEventListener('keypress', function(e) {
1097 1270 if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA' && e.target.tagName !== 'SELECT') {
1098 1271 e.preventDefault();
@@ -1099,19 +1272,19 @@
1099 1272 triggerSave();
1100 1273 }
1101 1274 });
1102 1275 }
1103 -
1276 +
1104 1277 // Email Settings submenu hover behavior for settings page
1105 1278 const emailNavLink = document.querySelector('.nav-link[href*="section=email"]');
1106 1279 const emailSubmenu = document.querySelector('.email-settings-submenu');
1107 -
1280 +
1108 1281 if (emailNavLink && emailSubmenu) {
1109 1282 // Check if any Email Settings subsection is active
1110 1283 const activeSubmenuLink = document.querySelector('.email-submenu-link.text-indigo-700');
1111 1284 const isEmailSectionActive = emailNavLink.classList.contains('text-indigo-700');
1112 1285 const isAnySubsectionActive = activeSubmenuLink !== null;
1113 -
1286 +
1114 1287 // If any subsection is active, show submenu permanently and disable hover effects
1115 1288 if (isAnySubsectionActive || isEmailSectionActive) {
1116 1289 emailSubmenu.style.display = 'block';
1117 1290 emailSubmenu.style.opacity = '1';
@@ -1119,9 +1292,9 @@
1119 1292 emailSubmenu.style.maxHeight = '200px';
1120 1293 emailSubmenu.classList.add('show');
1121 1294 return; // Exit early, no hover effects needed
1122 1295 }
1123 -
1296 +
1124 1297 // Only add hover effects if no subsection is active
1125 1298 // Show submenu on hover
1126 1299 emailNavLink.addEventListener('mouseenter', function() {
1127 1300 emailSubmenu.style.display = 'block';
@@ -1131,9 +1304,9 @@
1131 1304 emailSubmenu.style.visibility = 'visible';
1132 1305 emailSubmenu.style.maxHeight = '200px';
1133 1306 }, 10);
1134 1307 });
1135 -
1308 +
1136 1309 // Hide submenu when mouse leaves
1137 1310 emailNavLink.addEventListener('mouseleave', function() {
1138 1311 // Only hide if not hovering over submenu
1139 1312 setTimeout(() => {
@@ -1147,9 +1320,9 @@
1147 1320 }, 200);
1148 1321 }
1149 1322 }, 100);
1150 1323 });
1151 -
1324 +
1152 1325 // Keep submenu open when hovering over submenu items
1153 1326 emailSubmenu.addEventListener('mouseenter', function() {
1154 1327 emailSubmenu.style.opacity = '1';
1155 1328 emailSubmenu.style.visibility = 'visible';
@@ -1155,9 +1328,9 @@
1155 1328 emailSubmenu.style.visibility = 'visible';
1156 1329 emailSubmenu.style.maxHeight = '200px';
1157 1330 emailSubmenu.classList.add('show');
1158 1331 });
1159 -
1332 +
1160 1333 emailSubmenu.addEventListener('mouseleave', function() {
1161 1334 emailSubmenu.style.opacity = '0';
1162 1335 emailSubmenu.style.visibility = 'hidden';
1163 1336 emailSubmenu.style.maxHeight = '0';
@@ -1211,5 +1384,5 @@
1211 1384
1212 1385 .email-submenu-link.bg-indigo-50 svg {
1213 1386 color: rgb(79 70 229) !important;
1214 1387 }
1215 -</style>
1388 +</style>