PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.1
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Services / ReviewNoticeService.php

ReviewNoticeService.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.1, at includes/Services/ReviewNoticeService.php

558 lines 24.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Review Notice Service
4 *
5 * @package EasyInvoice
6 * @subpackage Services
7 */
8
9 namespace EasyInvoice\Services;
10
11 /**
12 * ReviewNoticeService Class
13 *
14 * Handles review notice display logic for free users
15 */
16 class ReviewNoticeService {
17
18 /**
19 * Notice option key for dismissal
20 */
21 const DISMISSED_OPTION = 'easy_invoice_review_notice_dismissed';
22
23 /**
24 * Notice option key for last shown timestamp
25 */
26 const LAST_SHOWN_OPTION = 'easy_invoice_review_notice_last_shown';
27
28 /**
29 * Notice option key to track if user actually interacted (clicked a button)
30 * This helps us distinguish between old auto-set values and user interactions
31 */
32 const USER_INTERACTED_OPTION = 'easy_invoice_review_notice_user_interacted';
33
34 /**
35 * Minimum documents required to show notice
36 */
37 const MIN_DOCUMENTS = 2;
38
39 /**
40 * Days to wait before showing again if skipped
41 */
42 const RESHOW_AFTER_DAYS = 7;
43
44 /**
45 * WordPress.org review URL
46 */
47 const REVIEW_URL = 'https://wordpress.org/support/plugin/easy-invoice/reviews/#new-post';
48
49 /**
50 * Initialize the review notice service
51 */
52 public static function init() {
53 $instance = new self();
54
55 // Add admin notice with high priority (before notices are removed)
56 // Note: EasyInvoice removes admin notices, so we add it early
57 add_action('admin_notices', [$instance, 'displayAdminNotice'], 5);
58
59 // Add custom UI notice (via filter to inject into template)
60 // Use priority 10 to ensure it runs
61 add_action('easy_invoice_admin_before_main_content', [$instance, 'displayCustomNotice'], 10);
62
63 // Handle AJAX actions
64 add_action('wp_ajax_easy_invoice_dismiss_review_notice', [$instance, 'handleDismissal']);
65 add_action('wp_ajax_easy_invoice_skip_review_notice', [$instance, 'handleSkip']);
66 }
67
68 /**
69 * Check if notice should be displayed
70 *
71 * @return bool
72 */
73 public function shouldDisplayNotice(): bool {
74 // Only show for free users
75 if (easy_invoice_has_pro()) {
76 return false;
77 }
78
79 $user_id = get_current_user_id();
80
81 // Don't show if dismissed (user clicked "Close")
82 if (get_user_meta($user_id, self::DISMISSED_OPTION, true)) {
83 return false;
84 }
85
86 // Check if user has created enough documents
87 $total_documents = $this->getTotalDocumentsCount();
88 if ($total_documents <= self::MIN_DOCUMENTS) {
89 return false;
90 }
91
92 // Check if we should reshow (if user clicked "Maybe Later" and 7 days passed)
93 // IMPORTANT: Only respect LAST_SHOWN_OPTION if user actually interacted (clicked a button)
94 // This prevents old auto-set values from blocking the notice
95 $user_interacted = get_user_meta($user_id, self::USER_INTERACTED_OPTION, true);
96 $last_shown = get_user_meta($user_id, self::LAST_SHOWN_OPTION, true);
97
98 // Only check LAST_SHOWN_OPTION if user has actually interacted with a button
99 // If LAST_SHOWN_OPTION exists but user_interacted is false, it means the old code set it
100 // In that case, we ignore it and show the notice (since user never clicked a button)
101 if ($user_interacted && $last_shown && (int)$last_shown > 0) {
102 $reshow_date = strtotime('+' . self::RESHOW_AFTER_DAYS . ' days', (int)$last_shown);
103
104 // Only block if it's been less than 7 days since user clicked "Maybe Later"
105 if (time() < $reshow_date) {
106 return false;
107 }
108 } elseif ($last_shown && !$user_interacted) {
109 // LAST_SHOWN_OPTION exists but user never interacted - this was set by old code
110 // Clear it silently and show the notice
111 delete_user_meta($user_id, self::LAST_SHOWN_OPTION);
112 }
113
114 // All checks passed - show the notice
115 return true;
116 }
117
118 /**
119 * Get total count of invoices and quotes
120 *
121 * @return int
122 */
123 private function getTotalDocumentsCount(): int {
124 global $wpdb;
125
126 // Count ALL invoices including drafts, published, pending, etc.
127 // Query directly to get accurate count of all post statuses
128 $invoice_count = (int) $wpdb->get_var($wpdb->prepare(
129 "SELECT COUNT(*) FROM {$wpdb->posts}
130 WHERE post_type = %s
131 AND post_status != 'trash'",
132 \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE
133 ));
134
135 // Count ALL quotes including drafts, published, pending, etc.
136 $quote_count = (int) $wpdb->get_var($wpdb->prepare(
137 "SELECT COUNT(*) FROM {$wpdb->posts}
138 WHERE post_type = %s
139 AND post_status != 'trash'",
140 \EasyInvoice\Constants\PostTypes::EASY_INVOICE_QUOTE_POST_TYPE
141 ));
142
143 return $invoice_count + $quote_count;
144 }
145
146 /**
147 * Display admin notice (WordPress admin UI)
148 * Shows on ALL WordPress admin pages (Dashboard, Posts, Pages, etc.)
149 */
150 public function displayAdminNotice() {
151 if (!$this->shouldDisplayNotice()) {
152 return;
153 }
154
155 // Show on all WordPress admin pages
156 $this->renderNotice();
157
158 // DO NOT update LAST_SHOWN_OPTION here - only update when user clicks a button
159 }
160
161 /**
162 * Display custom notice (plugin's custom UI)
163 *
164 * @param string $page Current page slug
165 */
166 public function displayCustomNotice($page = '') {
167 if (!$this->shouldDisplayNotice()) {
168 return;
169 }
170
171 // Add wrapper div for proper spacing
172 echo '<div style="padding: 0 20px; padding-top: 20px;">';
173 $this->renderCustomNotice();
174 echo '</div>';
175
176 // DO NOT update LAST_SHOWN_OPTION here - only update when user clicks a button
177 }
178
179 /**
180 * Render WordPress admin notice
181 */
182 private function renderNotice() {
183 ?>
184 <div class="notice notice-info easy-invoice-review-notice" id="easy-invoice-review-notice-admin" role="status" aria-live="polite">
185 <div class="easy-invoice-review-notice-content" style="display: flex; align-items: center; gap: 15px; padding: 10px 0;">
186 <div style="flex: 1;">
187 <p style="margin: 0; font-size: 14px;">
188 <strong><?php esc_html_e('Enjoying Easy Invoice?', 'easy-invoice'); ?></strong>
189 <?php esc_html_e('If you find Easy Invoice helpful, would you mind taking a moment to leave us a review on WordPress.org? It really helps us grow and improve!', 'easy-invoice'); ?>
190 </p>
191 </div>
192 <div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
193 <a href="<?php echo esc_url(self::REVIEW_URL); ?>"
194 target="_blank"
195 rel="noopener noreferrer"
196 class="button button-primary easy-invoice-leave-review"
197 aria-label="<?php esc_attr_e('Leave a review on WordPress.org', 'easy-invoice'); ?>"
198 style="margin-right: 5px;">
199 <?php esc_html_e('Leave a Review', 'easy-invoice'); ?>
200 </a>
201 <button type="button"
202 class="button easy-invoice-skip-review-notice"
203 aria-label="<?php esc_attr_e('Remind me later about leaving a review', 'easy-invoice'); ?>"
204 style="margin-right: 5px;">
205 <?php esc_html_e('Maybe Later', 'easy-invoice'); ?>
206 </button>
207 <button type="button"
208 class="button-link easy-invoice-close-review-notice"
209 aria-label="<?php esc_attr_e('Dismiss this notice permanently', 'easy-invoice'); ?>"
210 style="text-decoration: none; border: none; background: none; color: #2271b1; cursor: pointer;">
211 <?php esc_html_e('Close', 'easy-invoice'); ?>
212 </button>
213 </div>
214 </div>
215 </div>
216 <script>
217 (function($) {
218 'use strict';
219
220 $(document).ready(function() {
221 var $adminNotice = $('#easy-invoice-review-notice-admin');
222
223 // Handle "Leave a Review" - open review page and mark as shown (skip)
224 $(document).on('click', '.easy-invoice-leave-review', function(e) {
225 e.preventDefault();
226 var $link = $(this);
227 var originalHtml = $link.html();
228 var isDisabled = $link.prop('disabled');
229
230 if (isDisabled) {
231 return;
232 }
233
234 $link.prop('disabled', true).css('opacity', '0.7').css('pointer-events', 'none');
235
236 // Mark as shown so we don't show again for 7 days
237 $.ajax({
238 url: ajaxurl || '<?php echo esc_js(admin_url('admin-ajax.php')); ?>',
239 type: 'POST',
240 data: {
241 action: 'easy_invoice_skip_review_notice',
242 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_skip_review_notice')); ?>'
243 },
244 error: function() {
245 $link.prop('disabled', false).css('opacity', '1').css('pointer-events', 'auto');
246 }
247 });
248
249 // Open review page in new tab
250 var reviewUrl = '<?php echo esc_js(self::REVIEW_URL); ?>';
251 if (reviewUrl) {
252 window.open(reviewUrl, '_blank', 'noopener,noreferrer');
253 }
254 });
255
256 // Handle "Maybe Later" button
257 $(document).on('click', '.easy-invoice-skip-review-notice', function(e) {
258 e.preventDefault();
259 var $button = $(this);
260 var originalHtml = $button.html();
261 var isDisabled = $button.prop('disabled');
262
263 if (isDisabled) {
264 return;
265 }
266
267 $button.prop('disabled', true).css('opacity', '0.7');
268
269 $.ajax({
270 url: ajaxurl || '<?php echo esc_js(admin_url('admin-ajax.php')); ?>',
271 type: 'POST',
272 data: {
273 action: 'easy_invoice_skip_review_notice',
274 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_skip_review_notice')); ?>'
275 },
276 success: function() {
277 $adminNotice.fadeOut(300, function() {
278 $(this).remove();
279 });
280 },
281 error: function() {
282 $button.prop('disabled', false).css('opacity', '1').html(originalHtml);
283 }
284 });
285 });
286
287 // Handle "Close" button - dismiss permanently
288 $(document).on('click', '.easy-invoice-close-review-notice', function(e) {
289 e.preventDefault();
290 var $button = $(this);
291 var originalHtml = $button.html();
292 var isDisabled = $button.prop('disabled');
293
294 if (isDisabled) {
295 return;
296 }
297
298 $button.prop('disabled', true).css('opacity', '0.7');
299
300 $.ajax({
301 url: ajaxurl || '<?php echo esc_js(admin_url('admin-ajax.php')); ?>',
302 type: 'POST',
303 data: {
304 action: 'easy_invoice_dismiss_review_notice',
305 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_dismiss_review_notice')); ?>'
306 },
307 success: function() {
308 $adminNotice.fadeOut(300, function() {
309 $(this).remove();
310 });
311 },
312 error: function() {
313 $button.prop('disabled', false).css('opacity', '1').html(originalHtml);
314 }
315 });
316 });
317 });
318 })(jQuery);
319 </script>
320 <?php
321 }
322
323 /**
324 * Render custom UI notice
325 */
326 private function renderCustomNotice() {
327 ?>
328 <div class="easy-invoice-review-notice-custom" id="easy-invoice-review-notice-custom" role="status" aria-live="polite" style="margin: 20px 0; padding: 16px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px; color: white; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
329 <div style="display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap;">
330 <div style="flex: 1; min-width: 250px;">
331 <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px;">
332 <svg style="width: 24px; height: 24px; fill: currentColor;" viewBox="0 0 24 24" aria-hidden="true">
333 <path d="M12 2l3.09 6.26L22 9l-5.91 3.74L18.18 21 12 17.27 5.82 21l1.73-8.26L2 9l6.91-.74L12 2z"/>
334 </svg>
335 <h3 style="margin: 0; font-size: 18px; font-weight: 600;">
336 <?php esc_html_e('Enjoying Easy Invoice?', 'easy-invoice'); ?>
337 </h3>
338 </div>
339 <p style="margin: 0; font-size: 14px; opacity: 0.95;">
340 <?php esc_html_e('If you find Easy Invoice helpful, would you mind taking a moment to leave us a review on WordPress.org? It really helps us grow and improve!', 'easy-invoice'); ?>
341 </p>
342 </div>
343 <div style="display: flex; gap: 12px; align-items: center; flex-shrink: 0; flex-wrap: wrap;">
344 <a href="<?php echo esc_url(self::REVIEW_URL); ?>"
345 target="_blank"
346 rel="noopener noreferrer"
347 class="easy-invoice-leave-review-custom easy-invoice-review-btn"
348 aria-label="<?php esc_attr_e('Leave a review on WordPress.org', 'easy-invoice'); ?>"
349 style="background: white; color: #667eea; border: none; padding: 10px 20px; border-radius: 6px; text-decoration: none; font-weight: 600; transition: transform 0.2s ease, box-shadow 0.2s ease; display: inline-block;">
350 <?php esc_html_e('Leave a Review', 'easy-invoice'); ?>
351 <span style="margin-left: 6px;" aria-hidden="true"></span>
352 </a>
353 <button type="button"
354 class="easy-invoice-skip-review-notice-custom easy-invoice-review-btn"
355 aria-label="<?php esc_attr_e('Remind me later about leaving a review', 'easy-invoice'); ?>"
356 style="background: rgba(255, 255, 255, 0.2); color: white; border: 1px solid rgba(255, 255, 255, 0.3); padding: 10px 16px; border-radius: 6px; cursor: pointer; font-weight: 500; transition: background 0.2s ease;">
357 <?php esc_html_e('Maybe Later', 'easy-invoice'); ?>
358 </button>
359 <button type="button"
360 class="easy-invoice-close-review-notice-custom easy-invoice-review-btn"
361 aria-label="<?php esc_attr_e('Dismiss this notice permanently', 'easy-invoice'); ?>"
362 style="background: rgba(255, 255, 255, 0.1); color: white; border: 1px solid rgba(255, 255, 255, 0.2); padding: 10px 16px; border-radius: 6px; cursor: pointer; font-weight: 500; transition: background 0.2s ease;">
363 <?php esc_html_e('Close', 'easy-invoice'); ?>
364 </button>
365 </div>
366 </div>
367 </div>
368 <style>
369 .easy-invoice-review-btn:hover {
370 transform: translateY(-1px);
371 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
372 }
373 .easy-invoice-leave-review-custom:hover {
374 transform: scale(1.02) translateY(-1px);
375 box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
376 }
377 .easy-invoice-skip-review-notice-custom:hover {
378 background: rgba(255, 255, 255, 0.3) !important;
379 }
380 .easy-invoice-close-review-notice-custom:hover {
381 background: rgba(255, 255, 255, 0.2) !important;
382 }
383 @media (max-width: 768px) {
384 .easy-invoice-review-notice-custom > div {
385 flex-direction: column;
386 align-items: flex-start !important;
387 }
388 .easy-invoice-review-notice-custom > div > div:last-child {
389 width: 100%;
390 justify-content: flex-start;
391 }
392 }
393 </style>
394 <script>
395 (function($) {
396 'use strict';
397
398 $(document).ready(function() {
399 var $customNotice = $('#easy-invoice-review-notice-custom');
400
401 // Handle "Leave a Review" - open review page and mark as shown (skip)
402 $(document).on('click', '.easy-invoice-leave-review-custom', function(e) {
403 e.preventDefault();
404 var $link = $(this);
405 var originalHtml = $link.html();
406 var isDisabled = $link.prop('disabled') || $link.css('pointer-events') === 'none';
407
408 if (isDisabled) {
409 return;
410 }
411
412 $link.prop('disabled', true).css('opacity', '0.7').css('pointer-events', 'none');
413
414 // Mark as shown so we don't show again for 7 days
415 $.ajax({
416 url: ajaxurl || '<?php echo esc_js(admin_url('admin-ajax.php')); ?>',
417 type: 'POST',
418 data: {
419 action: 'easy_invoice_skip_review_notice',
420 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_skip_review_notice')); ?>'
421 },
422 error: function() {
423 $link.prop('disabled', false).css('opacity', '1').css('pointer-events', 'auto');
424 }
425 });
426
427 // Open review page in new tab
428 var reviewUrl = '<?php echo esc_js(self::REVIEW_URL); ?>';
429 if (reviewUrl) {
430 window.open(reviewUrl, '_blank', 'noopener,noreferrer');
431 }
432 });
433
434 // Handle "Maybe Later" button
435 $(document).on('click', '.easy-invoice-skip-review-notice-custom', function(e) {
436 e.preventDefault();
437 var $button = $(this);
438 var originalHtml = $button.html();
439 var isDisabled = $button.prop('disabled');
440
441 if (isDisabled) {
442 return;
443 }
444
445 $button.prop('disabled', true).css('opacity', '0.7');
446
447 $.ajax({
448 url: ajaxurl || '<?php echo esc_js(admin_url('admin-ajax.php')); ?>',
449 type: 'POST',
450 data: {
451 action: 'easy_invoice_skip_review_notice',
452 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_skip_review_notice')); ?>'
453 },
454 success: function() {
455 $customNotice.fadeOut(300, function() {
456 $(this).remove();
457 });
458 },
459 error: function() {
460 $button.prop('disabled', false).css('opacity', '1').html(originalHtml);
461 }
462 });
463 });
464
465 // Handle "Close" button - dismiss permanently
466 $(document).on('click', '.easy-invoice-close-review-notice-custom', function(e) {
467 e.preventDefault();
468 var $button = $(this);
469 var originalHtml = $button.html();
470 var isDisabled = $button.prop('disabled');
471
472 if (isDisabled) {
473 return;
474 }
475
476 $button.prop('disabled', true).css('opacity', '0.7');
477
478 $.ajax({
479 url: ajaxurl || '<?php echo esc_js(admin_url('admin-ajax.php')); ?>',
480 type: 'POST',
481 data: {
482 action: 'easy_invoice_dismiss_review_notice',
483 nonce: '<?php echo esc_attr(wp_create_nonce('easy_invoice_dismiss_review_notice')); ?>'
484 },
485 success: function() {
486 $customNotice.fadeOut(300, function() {
487 $(this).remove();
488 });
489 },
490 error: function() {
491 $button.prop('disabled', false).css('opacity', '1').html(originalHtml);
492 }
493 });
494 });
495 });
496 })(jQuery);
497 </script>
498 <?php
499 }
500
501 /**
502 * Handle AJAX dismissal (Close button - never show again)
503 */
504 public function handleDismissal() {
505 // Verify nonce and user capability
506 check_ajax_referer('easy_invoice_dismiss_review_notice', 'nonce');
507
508 if (!current_user_can('read')) {
509 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
510 return;
511 }
512
513 $user_id = get_current_user_id();
514
515 if (!$user_id) {
516 wp_send_json_error(['message' => __('User not found.', 'easy-invoice')]);
517 return;
518 }
519
520 // Mark that user has interacted
521 update_user_meta($user_id, self::USER_INTERACTED_OPTION, true);
522
523 // Save dismissal state per user (permanent)
524 update_user_meta($user_id, self::DISMISSED_OPTION, true);
525
526 wp_send_json_success(['message' => __('Review notice dismissed.', 'easy-invoice')]);
527 }
528
529 /**
530 * Handle AJAX skip (Maybe Later button - show again after 7 days)
531 */
532 public function handleSkip() {
533 // Verify nonce and user capability
534 check_ajax_referer('easy_invoice_skip_review_notice', 'nonce');
535
536 if (!current_user_can('read')) {
537 wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'easy-invoice')]);
538 return;
539 }
540
541 $user_id = get_current_user_id();
542
543 if (!$user_id) {
544 wp_send_json_error(['message' => __('User not found.', 'easy-invoice')]);
545 return;
546 }
547
548 // Mark that user has interacted
549 update_user_meta($user_id, self::USER_INTERACTED_OPTION, true);
550
551 // Update last shown timestamp - will reshow after 7 days
552 update_user_meta($user_id, self::LAST_SHOWN_OPTION, time());
553
554 wp_send_json_success(['message' => __('Review notice skipped. We\'ll remind you again later!', 'easy-invoice')]);
555 }
556 }
557
558