PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.0
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.0
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
← All changes | templates/settings-page.php +357 -202 2.1.132.4.0 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();
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>';
18 34
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 - '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
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(),
@@ -93,33 +142,32 @@
93 142 echo '</div>';
94 143 } else {
95 144 // Standard label for other types, or checkboxes without the special description structure
96 145 if ($type !== 'checkbox') {
97 - echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700 mb-2">' . $label . '</label>';
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)
@@ -267,9 +315,9 @@
267 315 * Note: This filter is already applied in the controller, so this is redundant
268 316 */
269 317 // $settings_config = apply_filters('easy_invoice_settings_fields_config', $settings_config);
270 318
271 -$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
272 320 if (!isset($settings_config[$active_section])) {
273 321 $active_section = key($settings_config);
274 322 }
275 323 ?>
@@ -274,10 +322,10 @@
274 322 }
275 323 ?>
276 324
277 325 <div class="p-8">
278 - <!-- Header with Actions -->
279 - <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">
280 328 <div class="flex items-center justify-between">
281 329 <div>
282 330 <h1 class="text-2xl font-bold text-gray-900"><?php esc_html_e('Settings', 'easy-invoice'); ?></h1>
283 331 <p class="mt-1 text-sm text-gray-400"><?php esc_html_e('Configure your invoice management system', 'easy-invoice'); ?></p>
@@ -290,12 +338,12 @@
290 338 </div>
291 339 </div>
292 340 </div>
293 341
294 - <!-- Main Content -->
342 +
295 343 <div class="max-w-7xl mx-auto">
296 344 <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mt-8">
297 - <!-- Left Sidebar - Navigation -->
345 +
298 346 <div class="lg:col-span-1">
299 347 <div class="bg-white shadow-lg rounded-lg sticky top-24 border border-gray-200">
300 348 <nav class="space-y-2 p-4" aria-label="<?php esc_attr_e('Settings Navigation', 'easy-invoice'); ?>">
301 349 <?php foreach ($settings_config as $section_id => $section_data) : ?>
@@ -300,80 +348,23 @@
300 348 <nav class="space-y-2 p-4" aria-label="<?php esc_attr_e('Settings Navigation', 'easy-invoice'); ?>">
301 349 <?php foreach ($settings_config as $section_id => $section_data) : ?>
302 350 <div class="space-y-1">
303 351 <a href="?page=easy-invoice-settings&section=<?php echo esc_attr($section_id); ?>"
304 - 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'; ?>"
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'; ?>"
305 353 aria-selected="<?php echo $section_id === $active_section ? 'true' : 'false'; ?>"
306 354 role="tab">
307 355
308 - <?php if ($section_id === 'company'): ?>
309 - <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">
310 - <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"/>
311 - <polyline points="9,22 9,12 15,12 15,22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
312 - </svg>
313 - <?php elseif ($section_id === 'invoice'): ?>
314 - <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">
315 - <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"/>
316 - <path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
317 - <path d="M16 13H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
318 - <path d="M16 17H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
319 - <path d="M10 9H8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
320 - </svg>
321 - <?php elseif ($section_id === 'quote'): ?>
322 - <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">
323 - <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"/>
324 - <path d="M14 2V8H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
325 - <path d="M9 9H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
326 - <path d="M9 13H15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
327 - <path d="M9 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
328 - </svg>
329 - <?php elseif ($section_id === 'currency'): ?>
330 - <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">
331 - <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
332 - <path d="M12 1V23" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
333 - <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"/>
334 - </svg>
335 - <?php elseif ($section_id === 'tax'): ?>
336 - <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">
337 - <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
338 - <path d="M8 14L16 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
339 - <path d="M8 8L16 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
340 - <path d="M12 6V18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
341 - </svg>
342 - <?php elseif ($section_id === 'payment'): ?>
343 - <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">
344 - <rect x="1" y="4" width="22" height="16" rx="2" ry="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
345 - <line x1="1" y1="10" x2="23" y2="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
346 - </svg>
347 - <?php elseif ($section_id === 'email'): ?>
348 - <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">
349 - <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"/>
350 - <polyline points="22,6 12,13 2,6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
351 - </svg>
352 - <?php elseif ($section_id === 'advanced'): ?>
353 - <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">
354 - <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
355 - <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"/>
356 - </svg>
357 - <?php elseif ($section_id === 'text_settings'): ?>
358 - <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">
359 - <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"/>
360 - <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"/>
361 - <path d="M8 11H16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
362 - <path d="M8 15H12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
363 - <path d="M8 19H14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
364 - <path d="M12 11L14 13L12 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
365 - </svg>
366 - <?php else: ?>
367 - <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">
368 - <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
369 - <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"/>
370 - </svg>
371 - <?php endif; ?>
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 + ?>
372 363
373 364 <span class="flex-1"><?php echo esc_html($section_data['title']); ?></span>
374 365 <?php if ($section_id === 'email' && !empty($section_data['subsections'])): ?>
375 - <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">
376 367 <polyline points="6,9 12,15 18,9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
377 368 </svg>
378 369 <?php endif; ?>
379 370 </a>
@@ -384,11 +375,11 @@
384 375 // Only show active subsection if we're actually on an email settings page
385 376 $active_email_subsection = null;
386 377 if ($active_section === 'email') {
387 378 if (isset($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
388 - $active_email_subsection = $_GET['subsection'];
379 + $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
389 380 } elseif (!empty($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
390 - $active_email_subsection = $_GET['subsection'];
381 + $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
391 382 } else {
392 383 // Default to 'general' only if we're on email section but no specific subsection
393 384 $active_email_subsection = 'general';
394 385 }
@@ -399,9 +390,9 @@
399 390 <a href="?page=easy-invoice-settings&section=email&subsection=<?php echo esc_attr($subsection_id); ?>"
400 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'; ?>"
401 392 data-subsection="<?php echo esc_attr($subsection_id); ?>"
402 393 data-section="<?php echo esc_attr($section_id); ?>">
403 - <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">
404 395 <polyline points="9,18 15,12 9,6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
405 396 </svg>
406 397 <?php
407 398 // Display shorter names for better UX
@@ -434,9 +425,9 @@
434 425 </nav>
435 426 </div>
436 427 </div>
437 428
438 - <!-- Right Content - Settings -->
429 +
439 430 <div class="lg:col-span-2">
440 431 <form id="settings-form" method="post" action="">
441 432 <?php wp_nonce_field('easy_invoice_settings', 'easy_invoice_settings_nonce'); ?>
442 433 <input type="hidden" name="action" value="easy_invoice_save_settings">
@@ -448,20 +439,15 @@
448 439 aria-labelledby="tab-<?php echo esc_attr($section_id); ?>">
449 440 <div class="px-6 py-5 border-b border-gray-200">
450 441 <div class="flex items-center">
451 442 <div class="flex-shrink-0 bg-indigo-100 rounded-lg p-3">
452 - <?php if (isset($section_data['icon']) && !empty($section_data['icon'])): ?>
453 - <?php if (strpos($section_data['icon'], '<svg') === 0): ?>
454 - <?php echo $section_data['icon']; ?>
455 - <?php else: ?>
456 - <i class="<?php echo esc_attr($section_data['icon']); ?> text-indigo-600 text-xl"></i>
457 - <?php endif; ?>
458 - <?php else: ?>
459 - <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">
460 - <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
461 - <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"/>
462 - </svg>
463 - <?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 + ?>
464 450 </div>
465 451 <div class="ml-4">
466 452 <h2 class="text-lg font-medium text-gray-900"><?php echo esc_html($section_data['title']); ?></h2>
467 453 <p class="mt-1 text-sm text-gray-400"><?php echo esc_html($section_data['description']); ?></p>
@@ -474,11 +460,11 @@
474 460 <?php
475 461 // Determine the active subsection from the hash or default to 'general'
476 462 $active_email_subsection = 'general';
477 463 if (isset($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
478 - $active_email_subsection = $_GET['subsection'];
464 + $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
479 465 } elseif (!empty($_GET['subsection']) && isset($section_data['subsections'][$_GET['subsection']])) {
480 - $active_email_subsection = $_GET['subsection'];
466 + $active_email_subsection = sanitize_key(wp_unslash($_GET['subsection'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
481 467 }
482 468 if (!isset($section_data['subsections'][$active_email_subsection])) {
483 469 $active_email_subsection = array_key_first($section_data['subsections']);
484 470 }
@@ -488,9 +474,9 @@
488 474 // Render all email subsections so JS can show/hide them
489 475 foreach ($section_data['subsections'] as $subsection_key => $subsection_data) {
490 476 $is_active = ($subsection_key === $active_email_subsection) ? '' : 'hidden';
491 477 ?>
492 - <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); ?>">
493 479 <?php
494 480 // Display subsection title
495 481 $subsection_title = '';
496 482 switch ($subsection_key) {
@@ -516,8 +502,55 @@
516 502 <?php if (!empty($subsection_data['description'])): ?>
517 503 <p class="text-sm text-gray-400"><?php echo esc_html($subsection_data['description']); ?></p>
518 504 <?php endif; ?>
519 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; ?>
520 553 <div class="space-y-6">
521 554 <?php if (!empty($subsection_data['fields'])) {
522 555 foreach ($subsection_data['fields'] as $option_key => $field_config) {
523 556 $current_value = $settings[$option_key] ?? ($field_config['default'] ?? '');
@@ -531,9 +564,9 @@
531 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';
532 565 $col_span_class = esc_attr($field_config['col_span'] ?? 'sm:col-span-6');
533 566 echo '<div class="field-wrapper form-group">';
534 567 if ($field_type !== 'checkbox') {
535 - 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>';
536 569 }
537 570
538 571 // Define required and aria_describedby variables
539 572 $required = !empty($field_config['required']) ? 'required' : '';
@@ -543,21 +576,21 @@
543 576 case 'text':
544 577 case 'email':
545 578 case 'url':
546 579 case 'tel':
547 - 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 . '>';
548 581 break;
549 582 case 'number':
550 583 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
551 584 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
552 585 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
553 - 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 . '>';
554 587 break;
555 588 case 'textarea':
556 - 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>';
557 590 break;
558 591 case 'select':
559 - 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 . '>';
560 593 if (!empty($field_config['options']) && is_array($field_config['options'])) {
561 594 foreach ($field_config['options'] as $opt_val => $opt_label) {
562 595 echo '<option value="' . esc_attr($opt_val) . '" ' . selected($current_value, $opt_val, false) . '>' . esc_html($opt_label) . '</option>';
563 596 }
@@ -564,25 +597,25 @@
564 597 }
565 598 echo '</select>';
566 599 break;
567 600 case 'multiselect':
568 - 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) . '">';
569 602 if (!empty($field_config['options']) && is_array($field_config['options'])) {
570 603 foreach ($field_config['options'] as $opt_val => $opt_label) {
571 604 $selected = is_array($current_value) && in_array($opt_val, $current_value) ? 'selected="selected"' : '';
572 - 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>';
573 606 }
574 607 }
575 608 echo '</select>';
576 609 break;
577 610 case 'readonly':
578 - echo '<input type="text" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . $input_class . ' bg-gray-50" readonly>';
579 - 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) . '">';
580 613 break;
581 614 case 'checkbox':
582 615 echo '<div class="flex items-center">';
583 - 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">';
584 - 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>';
585 618 echo '</div>';
586 619 if (!empty($field_description)) {
587 620 echo '<p id="' . $field_id . '-description" class="mt-2 text-sm text-gray-400 leading-relaxed">' . esc_html($field_description) . '</p>';
588 621 }
@@ -589,14 +622,14 @@
589 622 break;
590 623 case 'image':
591 624 echo '<div class="mt-1 flex items-center">';
592 625 echo ' <span class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100">';
593 - 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' : '') . '">';
594 627 echo ' </span>';
595 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') . '">';
596 629 echo esc_html__('Change', 'easy-invoice');
597 630 echo '</button>';
598 - 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) . '">';
599 632 echo '</div>';
600 633 break;
601 634 case 'wp_editor':
602 635 wp_editor( $current_value, $field_id, ['textarea_name' => $field_name, 'teeny' => true, 'media_buttons' => false, 'textarea_rows' => 7, 'editor_class' => 'mt-1'] );
@@ -601,9 +634,9 @@
601 634 case 'wp_editor':
602 635 wp_editor( $current_value, $field_id, ['textarea_name' => $field_name, 'teeny' => true, 'media_buttons' => false, 'textarea_rows' => 7, 'editor_class' => 'mt-1'] );
603 636 // Handle description for wp_editor specifically
604 637 if (!empty($field_description)) {
605 - 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>';
606 639 }
607 640 break;
608 641 case 'test_email':
609 642 echo '<div class="mt-1">';
@@ -622,10 +655,10 @@
622 655 $is_payment_test = ($field_id === 'easy_invoice_payment_test_template');
623 656 $width_class = $is_payment_test ? 'w-[200px]' : 'w-64';
624 657 echo '<div class="mt-1">';
625 658 echo ' <div class="flex items-center space-x-3">';
626 - 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')) . '">';
627 - 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) . '">';
628 661 echo ' <i class="fas fa-envelope mr-2" aria-hidden="true"></i>';
629 662 echo ' ' . esc_html__('Test Template', 'easy-invoice');
630 663 echo ' </button>';
631 664 echo ' </div>';
@@ -644,13 +677,13 @@
644 677 echo ' <div id="' . $field_id . '_result" class="mt-2" style="display: none;"></div>';
645 678 echo '</div>';
646 679 break;
647 680 default:
648 - 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) . '">';
649 682 break;
650 683 }
651 684 if ($field_type !== 'checkbox' && $field_type !== 'wp_editor' && !empty($field_description)) {
652 - 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>';
653 686 }
654 687 echo '</div>'; // Close the field wrapper div
655 688 }
656 689 }
@@ -663,9 +696,9 @@
663 696 <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
664 697 <p class="text-sm font-medium text-blue-900 mb-3"><?php esc_html_e('Available Placeholders:', 'easy-invoice'); ?></p>
665 698 <div class="grid grid-cols-2 md:grid-cols-3 gap-2">
666 699 <?php if (in_array($subsection_key, ['invoice_available', 'payment_received', 'payment_reminder'])): ?>
667 - <!-- Invoice-specific placeholders -->
700 +
668 701 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{invoice_number}}</div>
669 702 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{due_date}}</div>
670 703 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{invoice_url}}</div>
671 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,15 +704,15 @@
671 704 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{payment_url}}</div>
672 705 <?php endif; ?>
673 706
674 707 <?php if (in_array($subsection_key, ['quote_available'])): ?>
675 - <!-- Quote-specific placeholders -->
708 +
676 709 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{quote_number}}</div>
677 710 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{expiry_date}}</div>
678 711 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{quote_url}}</div>
679 712 <?php endif; ?>
680 713
681 - <!-- Common placeholders for all templates -->
714 +
682 715 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_name}}</div>
683 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>
684 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>
685 718 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_email}}</div>
@@ -685,8 +718,9 @@
685 718 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_email}}</div>
686 719 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{client_address}}</div>
687 720 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{company_name}}</div>
688 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>
689 723 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{issue_date}}</div>
690 724 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{company_email}}</div>
691 725 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{company_phone}}</div>
692 726 <div class="bg-white border border-blue-200 rounded px-3 py-2 text-xs text-blue-700 font-mono">{{payment_terms}}</div>
@@ -704,19 +738,139 @@
704 738 <div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
705 739 <div class="flex items-center">
706 740 <i class="fas fa-info-circle text-blue-500 mr-2"></i>
707 741 <p class="text-sm text-blue-700">
708 - <?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'); ?>
709 743 </p>
710 744 </div>
711 745 </div>
712 746 <?php endif; ?>
713 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 +
714 868 <div id="sortable-gateways" class="space-y-5">
715 869 <?php foreach ($all_gateways_sorted as $gateway_id => $gateway) : ?>
716 870 <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); ?>">
717 871 <div class="flex items-center mb-3 space-x-3">
718 - <!-- Drag Handle -->
872 +
719 873 <div class="gateway-handle text-gray-400 hover:text-gray-600 cursor-move mr-2">
720 874 <i class="fas fa-grip-vertical"></i>
721 875 </div>
722 876
@@ -731,18 +885,18 @@
731 885 <label for="gateway-enable-<?php echo esc_attr($gateway_id); ?>" class="font-medium text-gray-900">
732 886 <?php echo esc_html(method_exists($gateway, 'getTitle') ? $gateway->getTitle() : $gateway_id); ?>
733 887 </label>
734 888 <?php if (method_exists($gateway, 'getDescription') && $gateway->getDescription()): ?>
735 - <span class="block text-sm text-gray-400"><?php echo esc_html($gateway->getDescription()); ?></span>
889 + <span class="block text-sm text-gray-400"><?php echo wp_kses_post($gateway->getDescription()); ?></span>
736 890 <?php endif; ?>
737 891 </div>
738 892 </div>
739 893
740 - <!-- Custom Display Name Field -->
894 +
741 895 <div class="gateway-settings <?php echo empty($payment_methods_enabled) || !in_array($gateway_id, $payment_methods_enabled) ? 'hidden' : ''; ?>">
742 896 <div class="mb-4">
743 897 <label for="gateway-display-name-<?php echo esc_attr($gateway_id); ?>" class="block text-sm font-medium text-gray-700">
744 - <?php _e('Display Name', 'easy-invoice'); ?>
898 + <?php esc_html_e('Display Name', 'easy-invoice'); ?>
745 899 </label>
746 900 <input type="text"
747 901 id="gateway-display-name-<?php echo esc_attr($gateway_id); ?>"
748 902 name="settings[easy_invoice_gateway_display_name_<?php echo esc_attr($gateway_id); ?>]"
@@ -749,9 +903,9 @@
749 903 value="<?php echo esc_attr($settings['easy_invoice_gateway_display_name_' . $gateway_id] ?? (method_exists($gateway, 'getTitle') ? $gateway->getTitle() : ucfirst(str_replace('_', ' ', $gateway_id)))); ?>"
750 904 placeholder="<?php echo esc_attr(method_exists($gateway, 'getTitle') ? $gateway->getTitle() : ucfirst(str_replace('_', ' ', $gateway_id))); ?>"
751 905 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">
752 906 <p class="mt-1 text-xs text-gray-500">
753 - <?php _e('Leave blank to use the default name. This name will appear on invoices and payment forms.', 'easy-invoice'); ?>
907 + <?php esc_html_e('Leave blank to use the default name. This name will appear on invoices and payment forms.', 'easy-invoice'); ?>
754 908 </p>
755 909 </div>
756 910
757 911 <?php
@@ -833,18 +987,18 @@
833 987 case 'text':
834 988 case 'email':
835 989 case 'url':
836 990 case 'tel':
837 - 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'] ?? '') . '">';
991 + 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'] ?? '') . '">';
838 992 break;
839 993 case 'number':
840 994 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
841 995 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
842 996 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
843 - 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'] ?? '') . '">';
997 + 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'] ?? '') . '">';
844 998 break;
845 999 case 'select':
846 - echo '<select id="' . esc_attr($tax_key) . '" name="settings[' . esc_attr($tax_key) . ']" class="' . $select_class . '">';
1000 + echo '<select id="' . esc_attr($tax_key) . '" name="settings[' . esc_attr($tax_key) . ']" class="' . esc_html($select_class) . '">';
847 1001 if (!empty($field_config['options']) && is_array($field_config['options'])) {
848 1002 foreach ($field_config['options'] as $opt_val => $opt_label) {
849 1003 echo '<option value="' . esc_attr($opt_val) . '" ' . selected($current_value, $opt_val, false) . '>' . esc_html($opt_label) . '</option>';
850 1004 }
@@ -862,12 +1016,13 @@
862 1016 // Allow additional fields to be added to the tax section
863 1017 $additional_tax_fields = apply_filters('easy_invoice_tax_section_additional_fields', [], $settings);
864 1018 if (!empty($additional_tax_fields)) {
865 1019 echo '<div class="mt-6 pt-6 border-t border-gray-200">';
866 - echo '<h4 class="text-sm font-medium text-gray-900 mb-4">' . __('Additional Tax Settings', 'easy-invoice-pro') . '</h4>';
1020 + echo '<h4 class="text-sm font-medium text-gray-900 mb-4">' . esc_html__('Additional Tax Settings', 'easy-invoice') . '</h4>';
867 1021 echo '<div class="grid grid-cols-1 md:grid-cols-2 gap-6">';
868 1022 foreach ($additional_tax_fields as $field_key => $field_config) {
869 - $current_value = $settings[$field_key] ?? ($field_config['default'] ?? '');
1023 + // Filter-only fields are not in the loaded settings array; read the option itself.
1024 + $current_value = array_key_exists($field_key, (array) $settings) ? $settings[$field_key] : get_option($field_key, $field_config['default'] ?? '');
870 1025 easy_invoice_render_field($field_key, $field_config, $current_value);
871 1026 }
872 1027 echo '</div>';
873 1028 echo '</div>';
@@ -916,11 +1071,11 @@
916 1071 $conditional_class .= ' depends-on-' . esc_attr($depends_key) . '-' . esc_attr($depends_value);
917 1072 }
918 1073 }
919 1074
920 - echo '<div class="form-group ' . $form_group_class . $conditional_class . '">';
1075 + echo '<div class="form-group ' . esc_html($form_group_class) . esc_html($conditional_class) . '">';
921 1076 if ($field_type !== 'checkbox') {
922 - echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700">' . $field_label . '</label>';
1077 + echo '<label for="' . $field_id . '" class="block text-sm font-medium text-gray-700">' . esc_html($field_label) . '</label>';
923 1078 }
924 1079
925 1080 // Define required and aria_describedby variables
926 1081 $required = !empty($field_config['required']) ? 'required' : '';
@@ -930,21 +1085,21 @@
930 1085 case 'text':
931 1086 case 'email':
932 1087 case 'url':
933 1088 case 'tel':
934 - 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 . '>';
1089 + 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 . '>';
935 1090 break;
936 1091 case 'number':
937 1092 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
938 1093 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
939 1094 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
940 - 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 . '>';
1095 + 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 . '>';
941 1096 break;
942 1097 case 'textarea':
943 - 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>';
1098 + 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>';
944 1099 break;
945 1100 case 'select':
946 - echo '<select id="' . $field_id . '" name="' . $field_name . '" class="' . $select_class . '" ' . $required . ' ' . $aria_describedby . '>';
1101 + echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '" class="' . esc_html($select_class) . '" ' . esc_html($required) . ' ' . $aria_describedby . '>';
947 1102 if (!empty($field_config['options']) && is_array($field_config['options'])) {
948 1103 foreach ($field_config['options'] as $opt_val => $opt_label) {
949 1104 echo '<option value="' . esc_attr($opt_val) . '" ' . selected(strtolower($current_value), strtolower($opt_val), false) . '>' . esc_html($opt_label) . '</option>';
950 1105 }
@@ -951,25 +1106,25 @@
951 1106 }
952 1107 echo '</select>';
953 1108 break;
954 1109 case 'multiselect':
955 - echo '<select id="' . $field_id . '" name="' . $field_name . '[]" class="' . $select_class . '" multiple="multiple" aria-label="' . esc_attr($field_label) . '">';
1110 + echo '<select id="' . $field_id . '" name="' . esc_html($field_name) . '[]" class="' . esc_html($select_class) . '" multiple="multiple" aria-label="' . esc_attr($field_label) . '">';
956 1111 if (!empty($field_config['options']) && is_array($field_config['options'])) {
957 1112 foreach ($field_config['options'] as $opt_val => $opt_label) {
958 1113 $selected = is_array($current_value) && in_array($opt_val, $current_value) ? 'selected="selected"' : '';
959 - echo '<option value="' . esc_attr($opt_val) . '" ' . $selected . '>' . esc_html($opt_label) . '</option>';
1114 + echo '<option value="' . esc_attr($opt_val) . '" ' . esc_html($selected) . '>' . esc_html($opt_label) . '</option>';
960 1115 }
961 1116 }
962 1117 echo '</select>';
963 1118 break;
964 1119 case 'readonly':
965 - echo '<input type="text" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . $input_class . ' bg-gray-50" readonly>';
966 - echo '<input type="hidden" name="' . $field_name . '" value="' . esc_attr($current_value) . '">';
1120 + echo '<input type="text" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . esc_html($input_class) . ' bg-gray-50" readonly>';
1121 + echo '<input type="hidden" name="' . esc_html($field_name) . '" value="' . esc_attr($current_value) . '">';
967 1122 break;
968 1123 case 'checkbox':
969 1124 echo '<div class="flex items-center">';
970 - 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">';
971 - echo ' <label for="' . $field_id . '" class="ml-2 block text-sm text-gray-900">' . $field_label . '</label>';
1125 + 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">';
1126 + echo ' <label for="' . $field_id . '" class="ml-2 block text-sm text-gray-900">' . esc_html($field_label) . '</label>';
972 1127 echo '</div>';
973 1128 if (!empty($field_description)) {
974 1129 echo '<p id="' . $field_id . '-description" class="mt-2 text-sm text-gray-400 leading-relaxed">' . esc_html($field_description) . '</p>';
975 1130 }
@@ -976,14 +1131,14 @@
976 1131 break;
977 1132 case 'image':
978 1133 echo '<div class="mt-1 flex items-center">';
979 1134 echo ' <span class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100">';
980 - 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' : '') . '">';
1135 + 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' : '') . '">';
981 1136 echo ' </span>';
982 1137 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') . '">';
983 1138 echo esc_html__('Change', 'easy-invoice');
984 1139 echo '</button>';
985 - echo ' <input type="hidden" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
1140 + echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
986 1141 echo '</div>';
987 1142 break;
988 1143 case 'wp_editor':
989 1144 wp_editor( $current_value, $field_id, ['textarea_name' => $field_name, 'teeny' => true, 'media_buttons' => false, 'textarea_rows' => 7, 'editor_class' => 'mt-1'] );
@@ -988,25 +1143,25 @@
988 1143 case 'wp_editor':
989 1144 wp_editor( $current_value, $field_id, ['textarea_name' => $field_name, 'teeny' => true, 'media_buttons' => false, 'textarea_rows' => 7, 'editor_class' => 'mt-1'] );
990 1145 // Handle description for wp_editor specifically
991 1146 if (!empty($field_description)) {
992 - echo '<p class="mt-1 text-sm text-gray-400">' . esc_html($field_description) . '</p>';
1147 + 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>';
993 1148 }
994 1149 break;
995 1150 case 'color':
996 - 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">';
1151 + 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">';
997 1152 break;
998 1153 case 'range':
999 1154 $min = isset($field_config['min']) ? ' min="' . esc_attr($field_config['min']) . '"' : '';
1000 1155 $max = isset($field_config['max']) ? ' max="' . esc_attr($field_config['max']) . '"' : '';
1001 1156 $step = isset($field_config['step']) ? ' step="' . esc_attr($field_config['step']) . '"' : '';
1002 - echo '<input type="range" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="w-full">';
1157 + echo '<input type="range" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '"' . $min . $max . $step . ' class="w-full">';
1003 1158 echo '<span class="text-sm text-gray-400">' . esc_html($current_value) . '%</span>';
1004 1159 break;
1005 1160 case 'image_upload':
1006 1161 echo '<div class="image-upload-wrap">';
1007 1162 echo '<div class="flex items-center space-x-3">';
1008 - echo ' <input type="hidden" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
1163 + echo ' <input type="hidden" name="' . esc_html($field_name) . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '">';
1009 1164 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 . '">';
1010 1165 echo esc_html__('Upload Image', 'easy-invoice');
1011 1166 echo '</button>';
1012 1167 echo '</div>';
@@ -1023,18 +1178,18 @@
1023 1178 $button_class_name = 'regenerate-invoice-numbers-button';
1024 1179 if ($option_key === 'easy_invoice_regenerate_quote_numbers') {
1025 1180 $button_class_name = 'regenerate-quote-numbers-button';
1026 1181 }
1027 - echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' ' . $button_class_name . '">';
1182 + echo '<button type="button" id="' . $field_id . '" class="' . esc_attr($button_class) . ' ' . wp_kses_post($button_class_name) . '">';
1028 1183 echo esc_html($button_text);
1029 1184 echo '</button>';
1030 1185 break;
1031 1186 default:
1032 - echo '<input type="text" name="' . $field_name . '" id="' . $field_id . '" value="' . esc_attr($current_value) . '" class="' . $input_class . '" placeholder="' . $field_placeholder . '">';
1187 + 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) . '">';
1033 1188 break;
1034 1189 }
1035 1190 if ($field_type !== 'checkbox' && $field_type !== 'wp_editor' && !empty($field_description)) {
1036 - echo '<p class="mt-1 text-sm text-gray-400">' . esc_html($field_description) . '</p>';
1191 + 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>';
1037 1192 }
1038 1193 echo '</div>';
1039 1194 }
1040 1195 if ($is_grid) echo '</div>';
@@ -1050,9 +1205,9 @@
1050 1205 </div>
1051 1206 </div>
1052 1207 <?php endforeach; ?>
1053 1208
1054 - <!-- Bottom Save Button -->
1209 +
1055 1210 <div class="mt-8 pt-6 border-t border-gray-200">
1056 1211 <div class="flex items-center justify-start space-x-3">
1057 1212 <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'); ?>">
1058 1213 <i class="fas fa-save mr-2" aria-hidden="true"></i>