PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
← All changes | includes/admin/notifications/class-charitable-notifications.php +709 -201 1.7.0.3 → 1.8.13 View file →
@@ -1,14 +1,14 @@
1 1 <?php
2 2 /**
3 - * Admin form model class.
3 + * Charitable Admin Plugin Notifications.
4 4 *
5 5 * @package Charitable/Classes/Charitable_Admin_Form
6 6 * @author David Bisset
7 - * @copyright Copyright (c) 2022, WP Charitable LLC
7 + * @copyright Copyright (c) 2023, WP Charitable LLC
8 8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 - * @since 1.5.0
10 - * @version 1.5.0
9 + * @since 1.8.3
10 + * @version 1.8.3
11 11 */
12 12
13 13 // Exit if accessed directly.
14 14 if ( ! defined( 'ABSPATH' ) ) {
@@ -17,9 +17,10 @@
17 17
18 18 /**
19 19 * Notifications.
20 20 *
21 - * @since 1.7.5
21 + * @since 1.8.3
22 + * @version 1.8.3
22 23 */
23 24 class Charitable_Notifications {
24 25
25 26 /**
@@ -31,27 +32,27 @@
31 32
32 33 /**
33 34 * Source of notifications content.
34 35 *
35 - * @since 1.7.5
36 + * @since 1.8.3
36 37 *
37 38 * @var string
38 39 */
39 - const SOURCE_URL = 'https://plugin.wpcharitable.com/wp-content/notifications.json';
40 + const SOURCE_URL = 'https://plugin.wpcharitable.com/wp-content/charitable-notifications.json';
40 41
41 42 /**
42 43 * Array of license types, that are considered being Elite level.
43 44 *
44 - * @since 1.7.5
45 + * @since 1.8.3
45 46 *
46 47 * @var array
47 48 */
48 - const LICENSES_ELITE = [ 'agency', 'pro' ];
49 + const LICENSES_ELITE = [ 'agency', 'pro', 'elite' ];
49 50
50 51 /**
51 52 * Option value.
52 53 *
53 - * @since 1.7.5
54 + * @since 1.8.3
54 55 *
55 56 * @var bool|array
56 57 */
57 58 public $option = false;
@@ -58,9 +59,9 @@
58 59
59 60 /**
60 61 * Current license type.
61 62 *
62 - * @since 1.7.5
63 + * @since 1.8.3
63 64 *
64 65 * @var string
65 66 */
66 67 private $license_type;
@@ -65,8 +66,17 @@
65 66 */
66 67 private $license_type;
67 68
68 69 /**
70 + * Cached result of has_alert_notifications() for the current request.
71 + *
72 + * @since 1.8.12
73 + *
74 + * @var bool|null
75 + */
76 + private $has_alerts_cache = null;
77 +
78 + /**
69 79 * Returns and/or create the single instance of this class.
70 80 *
71 81 * @since 1.2.0
72 82 *
@@ -79,18 +89,24 @@
79 89
80 90 return self::$instance;
81 91 }
82 92
93 + /**
94 + * Constructor.
95 + *
96 + * @since 1.8.3
97 + */
83 98 public function __construct() {
84 99
85 - $this->init();
86 -
100 + if ( ! defined( 'CHARITABLE_DISABLE_NOTIFICATIONS' ) || ! CHARITABLE_DISABLE_NOTIFICATIONS ) {
101 + $this->init();
102 + }
87 103 }
88 104
89 105 /**
90 106 * Initialize class.
91 107 *
92 - * @since 1.7.5
108 + * @since 1.8.3
93 109 */
94 110 public function init() {
95 111
96 112 $this->hooks();
@@ -98,43 +114,21 @@
98 114
99 115 /**
100 116 * Register hooks.
101 117 *
102 - * @since 1.7.5
118 + * @since 1.8.3
103 119 */
104 120 public function hooks() {
105 -
106 - add_action( 'after_charitable_admin_enqueue_scripts', [ $this, 'enqueues' ], 10, 3 );
107 -
108 - // add_action( 'charitable_admin_overview_before_table', [ $this, 'output' ] ); // where the notifications comes out in the admin
109 - add_action( 'admin_notices', [ $this, 'output_notices' ], 99 ); // where the notifications comes out in the admin
110 - add_action( 'charitable_maybe_show_notification', [ $this, 'output' ], 99 ); // where the notifications comes out in the admin
111 - /*
112 - function my_cpt_info() {
113 - if ('edit-campaign' === get_current_screen()->id) {
114 - ?>
115 - <div class="updated">
116 - <p>
117 - <?php _e('Some information about my CPT...', 'my-text-domain'); ?>
118 - </p>
119 - </div>
120 - <?php
121 - }
122 - } // function my_cpt_info
123 - add_action('admin_notices', 'my_cpt_info');
124 - */
125 -
121 + add_action( 'charitable_maybe_show_plugin_notifications', [ $this, 'output' ], 99 ); // where the notifications comes out in the admin.
126 122 add_action( 'charitable_admin_notifications_update', [ $this, 'update' ] );
127 -
128 123 add_action( 'deactivate_plugin', [ $this, 'delete' ], 10, 2 );
129 -
130 - add_action( 'wp_ajax_charitable_notification_dismiss', [ $this, 'dismiss' ] );
124 + add_filter( 'charitable_notifications_get', [ $this, 'priority_sort' ], 99 );
131 125 }
132 126
133 127 /**
134 128 * Check if user has access and is enabled.
135 129 *
136 - * @since 1.7.5
130 + * @since 1.8.3
137 131 *
138 132 * @return bool
139 133 */
140 134 public function has_access() {
@@ -151,11 +145,64 @@
151 145 return (bool) apply_filters( 'charitable_admin_notifications_has_access', $access );
152 146 }
153 147
154 148 /**
149 + * Add notifications to the WordPress admin menu for Charitable.
150 + *
151 + * @since 1.8.3
152 + *
153 + * @param array $submenu Array of menu items.
154 + * @return bool
155 + */
156 + public function add_notifications_to_menu( $submenu = array() ) {
157 +
158 + if ( ( defined( 'CHARITABLE_NOTIFICATIONS_FORCE_OFF' ) && CHARITABLE_NOTIFICATIONS_FORCE_OFF ) ) {
159 + return $submenu;
160 + }
161 +
162 + $notifications_menu_item = array(
163 + 'page_title' => __( 'Notifications', 'charitable' ),
164 + 'menu_title' => __( 'Notifications', 'charitable' ),
165 + 'menu_slug' => 'charitable-notifications',
166 + 'function' => array( $this, 'render_plugin_notifications_page' ),
167 + 'capability' => 'manage_charitable_settings',
168 + );
169 +
170 + // if there are new notifications add the indicator to the menu title.
171 + if ( $this->get_new_notifications_count() ) {
172 + $dot_type = $this->has_alert_notifications() ? 'alert' : 'info';
173 + $notifications_menu_item['menu_title'] .= '<div class="charitable-menu-notification-indicator ' . $dot_type . '"></div>';
174 + }
175 +
176 + // If the checklist exists in the submenu, add this item after it.
177 + $checklist_key = array_search( 'charitable-setup-checklist', array_column( $submenu, 'menu_slug' ), true );
178 + if ( false !== $checklist_key ) {
179 + $submenu = array_merge(
180 + array_slice( $submenu, 0, $checklist_key + 1 ),
181 + array( $notifications_menu_item ),
182 + array_slice( $submenu, $checklist_key + 1 )
183 + );
184 + } else {
185 + array_unshift( $submenu, $notifications_menu_item );
186 + }
187 +
188 + return $submenu;
189 + }
190 +
191 + /**
192 + * Technically this should render the plugin notifications page.
193 + * However, notifications aren't rendering with this method. There should be a redirect, but the existance of this function prevents fatal errors.
194 + *
195 + * @since 1.8.3
196 + */
197 + public function render_plugin_notifications_page() {
198 + $this->output();
199 + }
200 +
201 + /**
155 202 * Get option value.
156 203 *
157 - * @since 1.7.5
204 + * @since 1.8.3
158 205 *
159 206 * @param bool $cache Reference property cache if available.
160 207 *
161 208 * @return array
@@ -168,12 +215,16 @@
168 215
169 216 $option = (array) get_option( 'charitable_notifications', [] );
170 217
171 218 $this->option = [
172 - 'update' => ! empty( $option['update'] ) ? (int) $option['update'] : 0,
173 - 'feed' => ! empty( $option['feed'] ) ? (array) $option['feed'] : [],
174 - 'events' => ! empty( $option['events'] ) ? (array) $option['events'] : [],
175 - 'dismissed' => ! empty( $option['dismissed'] ) ? (array) $option['dismissed'] : [],
219 + 'update' => ! empty( $option['update'] ) ? (int) $option['update'] : 0,
220 + 'feed' => ! empty( $option['feed'] ) ? (array) $option['feed'] : [],
221 + 'events' => ! empty( $option['events'] ) ? (array) $option['events'] : [],
222 + 'dismissed' => ! empty( $option['dismissed'] ) ? (array) $option['dismissed'] : [],
223 + 'local' => ! empty( $option['local'] ) ? (array) $option['local'] : [],
224 + 'new_feed' => ! empty( $option['new_feed'] ) ? (array) $option['new_feed'] : [],
225 + 'dismissed_times' => ! empty( $option['dismissed_times'] ) ? (array) $option['dismissed_times'] : [],
226 + 'settings' => ! empty( $option['settings'] ) ? (array) $option['settings'] : [],
176 227 ];
177 228
178 229 return $this->option;
179 230 }
@@ -180,20 +231,23 @@
180 231
181 232 /**
182 233 * Fetch notifications from feed.
183 234 *
184 - * @since 1.7.5
235 + * @since 1.8.3
185 236 *
186 237 * @return array
187 238 */
188 239 public function fetch_feed() {
189 240
241 + $args = [
242 + 'timeout' => 10,
243 + 'sslverify' => false,
244 + 'user-agent' => charitable_get_default_user_agent(),
245 + ];
246 +
190 247 $response = wp_remote_get(
191 248 self::SOURCE_URL,
192 - [
193 - 'timeout' => 10,
194 - 'user-agent' => charitable_get_default_user_agent(),
195 - ]
249 + $args
196 250 );
197 251
198 252 if ( is_wp_error( $response ) ) {
199 253 return [];
@@ -210,9 +264,9 @@
210 264
211 265 /**
212 266 * Verify notification data before it is saved.
213 267 *
214 - * @since 1.7.5
268 + * @since 1.8.3
215 269 *
216 270 * @param array $notifications Array of notifications items to verify.
217 271 *
218 272 * @return array
@@ -232,9 +286,9 @@
232 286 // 1. notification message is empty.
233 287 // 2. license type does not match.
234 288 // 3. notification is expired.
235 289 // 4. notification has already been dismissed.
236 - // 5. notification existed before installing WPCharitable.
290 + // 5. notification existed before installing Charitable.
237 291 // (Prevents bombarding the user with notifications after activation).
238 292 if (
239 293 empty( $notification['content'] ) ||
240 294 ! $this->is_license_type_match( $notification ) ||
@@ -241,9 +295,8 @@
241 295 $this->is_expired( $notification ) ||
242 296 $this->is_dismissed( $notification ) ||
243 297 $this->is_existed( $notification )
244 298 ) {
245 - // echo 'false';
246 299 continue;
247 300 }
248 301
249 302 $data[] = $notification;
@@ -248,10 +301,8 @@
248 301
249 302 $data[] = $notification;
250 303 }
251 304
252 - // print_r( $data ); exit;
253 -
254 305 return $data;
255 306 }
256 307
257 308 /**
@@ -256,9 +307,9 @@
256 307
257 308 /**
258 309 * Verify saved notification data for active notifications.
259 310 *
260 - * @since 1.7.5
311 + * @since 1.8.3
261 312 *
262 313 * @param array $notifications Array of notifications items to verify.
263 314 *
264 315 * @return array
@@ -286,14 +337,19 @@
286 337
287 338 /**
288 339 * Get notification data.
289 340 *
290 - * @since 1.7.5
341 + * @since 1.8.3
342 + * @version 1.8.12
291 343 *
292 344 * @return array
293 345 */
294 346 public function get() {
295 347
348 + if ( defined( 'CHARITABLE_DISABLE_NOTIFICATIONS' ) && CHARITABLE_DISABLE_NOTIFICATIONS ) {
349 + return [];
350 + }
351 +
296 352 if ( ! $this->has_access() ) {
297 353 return [];
298 354 }
299 355
@@ -300,44 +356,169 @@
300 356 $option = $this->get_option();
301 357
302 358 // Update notifications using async task.
303 359 if ( empty( $option['update'] ) || time() > $option['update'] + DAY_IN_SECONDS ) {
360 + $option = $this->update();
361 + } elseif ( empty( $option['feed'] ) && empty( $option['events'] ) ) {
362 + // regardless of the update time, if there are no notifications, update attempt.
363 + $option = $this->update();
364 + }
304 365
305 - $this->update();
366 + $legacy_enabled = ! class_exists( 'Charitable_Notification_Settings' ) ||
367 + Charitable_Notification_Settings::get_instance()->is_category_enabled( 'legacy_feed' );
306 368
307 - // $tasks = charitable()->get( 'tasks' );
369 + $feed = ( $legacy_enabled && ! empty( $option['feed'] ) ) ? $this->verify_active( $option['feed'] ) : [];
370 + $events = ( $legacy_enabled && ! empty( $option['events'] ) ) ? $this->verify_active( $option['events'] ) : [];
308 371
309 - // if ( ! $tasks->is_scheduled( 'charitable_admin_notifications_update' ) !== false ) {
310 - // $tasks
311 - // ->create( 'charitable_admin_notifications_update' )
312 - // ->async()
313 - // ->params()
314 - // ->register();
315 - // }
372 + $notifications = array_merge( $feed, $events );
373 +
374 + /**
375 + * Filter the final notifications array before output.
376 + *
377 + * Used by addons (e.g. Pro's Charitable_Dynamic_Notifications) to prepend license-state
378 + * notifications (expiring/expired) above the remote feed.
379 + *
380 + * @since 1.8.12
381 + *
382 + * @param array $notifications Merged feed + events notifications.
383 + */
384 + return (array) apply_filters( 'charitable_notifications_get', $notifications );
385 + }
386 +
387 + /**
388 + * Final sort — enforces spec priority order regardless of filter injection order.
389 + *
390 + * Order: new_feed (non-dismissible first) → license → health → legal →
391 + * lifecycle → milestone → nudge → everything else (legacy feed).
392 + *
393 + * @since 1.8.12
394 + *
395 + * @param array $notifications
396 + * @return array
397 + */
398 + public function priority_sort( array $notifications ) {
399 + $order = [
400 + 'new_feed_pinned' => [],
401 + 'new_feed' => [],
402 + 'license' => [],
403 + 'health' => [],
404 + 'legal' => [],
405 + 'lifecycle' => [],
406 + 'milestone' => [],
407 + 'nudge' => [],
408 + 'other' => [],
409 + ];
410 +
411 + $license_ids = [ 'license_expired', 'license_expiring' ];
412 +
413 + foreach ( $notifications as $n ) {
414 + $id = isset( $n['id'] ) ? (string) $n['id'] : '';
415 + $category = isset( $n['category'] ) ? $n['category'] : '';
416 +
417 + if ( 'new_feed' === $category ) {
418 + $dismissible = ! isset( $n['dismissible'] ) || (bool) $n['dismissible'];
419 + $order[ $dismissible ? 'new_feed' : 'new_feed_pinned' ][] = $n;
420 + } elseif ( in_array( $id, $license_ids, true ) ) {
421 + $order['license'][] = $n;
422 + } elseif ( isset( $order[ $category ] ) ) {
423 + $order[ $category ][] = $n;
424 + } else {
425 + $order['other'][] = $n;
426 + }
316 427 }
317 428
318 - $feed = ! empty( $option['feed'] ) ? $this->verify_active( $option['feed'] ) : [];
319 - $events = ! empty( $option['events'] ) ? $this->verify_active( $option['events'] ) : [];
429 + $result = [];
430 + foreach ( $order as $group ) {
431 + foreach ( $group as $item ) {
432 + $result[] = $item;
433 + }
434 + }
435 + return $result;
436 + }
320 437
321 - return array_merge( $feed, $events );
438 + /**
439 + * Get notification count in the feed, regardless if they are dismissed or not.
440 + *
441 + * @since 1.8.3
442 + *
443 + * @return int
444 + */
445 + public function get_count() {
446 + return count( $this->get() );
322 447 }
323 448
324 449 /**
325 - * Get notification count.
450 + * Get new notifications by comparing those in the feed vs dismissed.
326 451 *
327 - * @since 1.7.5
452 + * @since 1.8.3
328 453 *
329 454 * @return int
330 455 */
331 - public function get_count() {
456 + public function get_new_notifications_count() {
332 457
333 - return count( $this->get() );
458 + $option = $this->get_option();
459 + $dimissed_ids = ! empty( $option['dismissed'] ) ? $option['dismissed'] : [];
460 + $notification_feed = $this->get();
461 +
462 + // if $option is empty or if the feed is empty, there are no new notifications.
463 + if ( empty( $option ) || empty( $notification_feed ) ) {
464 + return 0;
465 + }
466 +
467 + $count = 0;
468 +
469 + foreach ( $notification_feed as $notification ) {
470 + // check the id of the notification against the dismissed ids.
471 + if ( ! in_array( (string) $notification['id'], $dimissed_ids, true ) ) {
472 + ++$count;
473 + }
474 + }
475 +
476 + return (int) $count;
334 477 }
335 478
336 479 /**
480 + * Check if any undismissed notifications are alerts (health or legal category).
481 + *
482 + * @since 1.8.12
483 + *
484 + * @return bool
485 + */
486 + public function has_alert_notifications() {
487 +
488 + if ( null !== $this->has_alerts_cache ) {
489 + return $this->has_alerts_cache;
490 + }
491 +
492 + $option = $this->get_option();
493 + $dismissed_ids = ! empty( $option['dismissed'] ) ? $option['dismissed'] : [];
494 + $notification_feed = $this->get();
495 +
496 + if ( empty( $notification_feed ) ) {
497 + $this->has_alerts_cache = false;
498 + return $this->has_alerts_cache;
499 + }
500 +
501 + $alert_categories = [ 'health', 'legal' ];
502 +
503 + foreach ( $notification_feed as $notification ) {
504 + if ( in_array( (string) $notification['id'], $dismissed_ids, true ) ) {
505 + continue;
506 + }
507 + if ( ! empty( $notification['category'] ) && in_array( $notification['category'], $alert_categories, true ) ) {
508 + $this->has_alerts_cache = true;
509 + return $this->has_alerts_cache;
510 + }
511 + }
512 +
513 + $this->has_alerts_cache = false;
514 + return $this->has_alerts_cache;
515 + }
516 +
517 + /**
337 518 * Add a new Event Driven notification.
338 519 *
339 - * @since 1.7.5
520 + * @since 1.8.3
340 521 *
341 522 * @param array $notification Notification data.
342 523 */
343 524 public function add( $notification ) {
@@ -366,9 +547,9 @@
366 547
367 548 /**
368 549 * Determine if notification data is valid.
369 550 *
370 - * @since 1.7.5
551 + * @since 1.8.3
371 552 *
372 553 * @param array $notification Notification data.
373 554 *
374 555 * @return bool
@@ -384,9 +565,9 @@
384 565
385 566 /**
386 567 * Determine if notification has already been dismissed.
387 568 *
388 - * @since 1.7.5
569 + * @since 1.8.3
389 570 *
390 571 * @param array $notification Notification data.
391 572 *
392 573 * @return bool
@@ -401,9 +582,9 @@
401 582
402 583 /**
403 584 * Determine if license type is match.
404 585 *
405 - * @since 1.7.5
586 + * @since 1.8.3
406 587 *
407 588 * @param array $notification Notification data.
408 589 *
409 590 * @return bool
@@ -420,9 +601,9 @@
420 601
421 602 /**
422 603 * Determine if notification is expired.
423 604 *
424 - * @since 1.7.5
605 + * @since 1.8.3
425 606 *
426 607 * @param array $notification Notification data.
427 608 *
428 609 * @return bool
@@ -427,16 +608,31 @@
427 608 *
428 609 * @return bool
429 610 */
430 611 private function is_expired( $notification ) {
612 + // Check if the notification 'end' parameter is set and not empty.
613 + if ( empty( $notification['end'] ) ) {
614 + return false;
615 + }
431 616
432 - return ! empty( $notification['end'] ) && time() > strtotime( $notification['end'] );
617 + // Get the WordPress timezone.
618 + $timezone = wp_timezone();
619 +
620 + // Convert the notification end date to a DateTime object in the WordPress timezone.
621 + $end_time = new DateTime( $notification['end'], $timezone );
622 +
623 + // Get the current server time as a DateTime object in the WordPress timezone.
624 + $current_time = new DateTime( 'now', $timezone );
625 +
626 + // Compare the current time with the notification end time.
627 + return $current_time > $end_time;
433 628 }
434 629
435 630 /**
436 - * Determine if notification existed before installing WPForms.
631 + * Determine if notification existed before installing Charitable.
632 + * If there is an "allow_on_activation" key, it will be used to determine to display even if the notification existed before.
437 633 *
438 - * @since 1.7.5
634 + * @since 1.8.3
439 635 *
440 636 * @param array $notification Notification data.
441 637 *
442 638 * @return bool
@@ -444,57 +640,83 @@
444 640 private function is_existed( $notification ) {
445 641
446 642 $activated = charitable_get_activated_timestamp();
447 643
448 - return ! empty( $activated ) &&
644 + if ( ! isset( $notification['allow_on_activation'] ) ) {
645 + return ! empty( $activated ) &&
449 646 ! empty( $notification['start'] ) &&
450 647 $activated > strtotime( $notification['start'] );
648 + } else {
649 + return ! empty( $activated ) &&
650 + ! empty( $notification['start'] ) &&
651 + false === $notification['allow_on_activation'];
652 + }
451 653 }
452 654
453 655 /**
454 656 * Update notification data from feed.
455 657 *
456 - * @since 1.7.5
658 + * @since 1.8.3
457 659 */
458 660 public function update() {
459 661
460 662 $option = $this->get_option();
461 - $data = [
462 - 'update' => time(),
463 - 'feed' => $this->fetch_feed(),
464 - 'events' => $option['events'],
465 - 'dismissed' => $option['dismissed'],
663 +
664 + $data = [
665 + 'update' => time(),
666 + 'feed' => $this->fetch_feed(),
667 + 'events' => $option['events'],
668 + 'dismissed' => $option['dismissed'],
669 + 'local' => $option['local'],
670 + 'new_feed' => $option['new_feed'],
671 + 'dismissed_times' => $option['dismissed_times'],
672 + 'settings' => $option['settings'],
466 673 ];
467 674
468 - // echo 'data:'; print_r( $data ); exit;
469 -
470 - // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
675 + // phpcs:disable Charitable.PHP.ValidateHooks.InvalidHookName
471 676 /**
472 677 * Allow changing notification data before it will be updated in database.
473 678 *
474 - * @since 1.7.5
679 + * @since 1.8.3
475 680 *
476 681 * @param array $data New notification data.
477 682 */
478 683 $data = (array) apply_filters( 'charitable_admin_notifications_update_data', $data );
479 - // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
684 + // phpcs:enable Charitable.PHP.ValidateHooks.InvalidHookName
480 685
481 686 update_option( 'charitable_notifications', $data );
687 +
688 + return $data;
482 689 }
483 690
484 691 /**
692 + * Save specific keys back to the option without triggering a full update.
693 + *
694 + * @since 1.8.12
695 + *
696 + * @param array $partial Associative array of keys to merge into the stored option.
697 + * Intended keys: 'local', 'new_feed', 'dismissed_times', 'settings'.
698 + * @return void
699 + */
700 + public function save_option_partial( array $partial ) {
701 + $option = $this->get_option( false ); // bypass cache
702 + $merged = array_merge( $option, $partial );
703 + update_option( 'charitable_notifications', $merged );
704 + $this->option = false; // invalidate cache
705 + }
706 +
707 + /**
485 708 * Remove notification data from database before a plugin is deactivated.
486 709 *
487 - * @since 1.7.5
710 + * @since 1.8.3
488 711 *
489 712 * @param string $plugin Path to the plugin file relative to the plugins directory.
490 713 * @param bool $network_deactivating Whether the plugin is deactivated for all sites in the network
491 714 * or just the current site. Multisite only. Default false.
492 715 */
493 - public function delete( $plugin, $network_deactivating ) {
716 + public function delete( $plugin, $network_deactivating ) { // phpcs:ignore
494 717
495 718 $charitable_plugins = [
496 - 'charitable-lite/charitable.php',
497 719 'charitable/charitable.php',
498 720 ];
499 721
500 722 if ( ! in_array( $plugin, $charitable_plugins, true ) ) {
@@ -504,57 +726,12 @@
504 726 delete_option( 'charitable_notifications' );
505 727 }
506 728
507 729 /**
508 - * Enqueue assets on Form Overview admin page.
730 + * Output notices.
509 731 *
510 - * @since 1.7.5
732 + * @since 1.8.3
511 733 */
512 - public function enqueues( $suffix, $version, $assets_dir ) {
513 -
514 - if ( ! $this->get_count() ) {
515 - return;
516 - }
517 -
518 - $suffix = false;
519 -
520 - wp_register_style(
521 - 'charitable-admin-notifications',
522 - $assets_dir . 'css/charitable-admin-notifications' . $suffix . '.css',
523 - array( 'charitable-lity' ),
524 - $version
525 - );
526 -
527 - wp_enqueue_style( 'charitable-admin-notifications' );
528 -
529 - wp_register_script(
530 - 'charitable-admin-notifications',
531 - $assets_dir . 'js/charitable-admin-notifications' . $suffix . '.js',
532 - array( 'jquery', 'charitable-lity' ),
533 - $version,
534 - true
535 - );
536 -
537 - wp_enqueue_script( 'charitable-admin-notifications' );
538 -
539 - // Lity.
540 - wp_enqueue_style(
541 - 'charitable-lity',
542 - $assets_dir . 'lib/lity/lity.min.css',
543 - [],
544 - $version
545 - );
546 -
547 - wp_enqueue_script(
548 - 'charitable-lity',
549 - $assets_dir . 'lib/lity/lity.min.js',
550 - [ 'jquery' ],
551 - $version,
552 - true
553 - );
554 -
555 - }
556 -
557 734 public function output_notices() {
558 735 return $this->output( 'notices' );
559 736 }
560 737
@@ -560,33 +737,39 @@
560 737
561 738 /**
562 739 * Output notifications on Form Overview admin area.
563 740 *
564 - * @since 1.7.5
741 + * @since 1.8.3
742 + * @version 1.8.12
743 + *
744 + * @param string $location Location where the notifications are output.
745 + * @param int $limit_active Limit of active notifications to show.
565 746 */
566 - public function output( $location = false ) {
747 + public function output( $location = false, $limit_active = 4 ) {
567 748
749 + if ( defined( 'CHARITABLE_DISABLE_NOTIFICATIONS' ) && CHARITABLE_DISABLE_NOTIFICATIONS ) {
750 + return;
751 + }
752 +
568 753 global $post;
569 754
570 - if ('notices' === $location && ! $this->show_notifications() ) {
755 + if ( 'notices' === $location && ! $this->show_notifications() ) {
571 756 return;
572 757 }
573 758
574 759 $notifications = $this->get();
575 760
576 - // print_r( $notifications ); exit;
577 -
578 - if ( empty( $notifications ) ) {
579 - return;
580 - }
581 -
582 - $notifications_html = '';
583 - $current_class = ' current';
584 - $content_allowed_tags = [
761 + $active_notifications_html = '';
762 + $dismissed_notifications_html = '';
763 + $active_notifications_count = 0;
764 + $dismissed_notifications_count = 0;
765 + $current_class = ' current';
766 + $content_allowed_tags = [
585 767 'em' => [],
586 768 'strong' => [],
587 769 'span' => [
588 770 'style' => [],
771 + 'class' => [],
589 772 ],
590 773 'a' => [
591 774 'href' => [],
592 775 'target' => [],
@@ -592,9 +775,29 @@
592 775 'target' => [],
593 776 'rel' => [],
594 777 ],
595 778 ];
779 + $template_location = $location === 'dashboard' ? '/admin/templates/notifications-dashboard' : '/admin/templates/notifications';
780 + $template_location = apply_filters( 'charitable_admin_notifications_template_location', $template_location, $location );
596 781
782 + if ( empty( $notifications ) ) {
783 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
784 + echo charitable_render(
785 + $template_location,
786 + [
787 + 'notifications' => [
788 + 'active_count' => $active_notifications_count,
789 + 'dismissed_count' => $dismissed_notifications_count,
790 + 'active_html' => $active_notifications_html,
791 + 'dismissed_html' => $dismissed_notifications_html,
792 + ],
793 + ],
794 + true
795 + );
796 + return;
797 + }
798 +
799 + // Get active notifications.
597 800 foreach ( $notifications as $notification ) {
598 801
599 802 // Prepare required arguments.
600 803 $notification = wp_parse_args(
@@ -613,45 +816,320 @@
613 816 if ( ! $title && ! $content ) {
614 817 continue;
615 818 }
616 819
617 - // Notification HTML.
618 - $notifications_html .= sprintf(
619 - '<div class="charitable-notifications-message%5$s" data-message-id="%4$s">
620 - <h3 class="charitable-notifications-title">%1$s%6$s</h3>
621 - <p class="charitable-notifications-content">%2$s</p>
622 - %3$s
623 - </div>',
624 - esc_html( $title ),
625 - wp_kses( $content, $content_allowed_tags ),
626 - $this->get_notification_buttons_html( $notification ),
627 - esc_attr( $notification['id'] ),
628 - esc_attr( $current_class ),
629 - $this->get_video_badge_html( $this->get_component_data( $notification['video'] ) )
630 - );
820 + // Non-dismissible notifications (e.g. expired license) always render as active. @since 1.8.12
821 + $is_dismissible = ! isset( $notification['dismissible'] ) || false !== $notification['dismissible'];
631 822
823 + // Dynamic notifications (those with an explicit 'dismissible' key) suppress the relative date. @since 1.8.12
824 + $is_dynamic = isset( $notification['dismissible'] );
825 + $date_string = ! empty( $notification['start'] ) ? $this->human_readable_date_from_mysql( $notification['start'] ) : '';
826 + $date_html = $date_string ? '<span class="charitable-notification-date">' . esc_html( $date_string ) . '</span>' : '';
827 +
828 + // If the notification ID is not a part of the dismissed notifications, add it to the list.
829 + if ( ! $is_dismissible || ! $this->is_dismissed( $notification ) ) {
830 +
831 + // Dismiss link is suppressed for non-dismissible notifications (e.g. expired license). @since 1.8.12
832 + $dismiss_link = $is_dismissible ? '<a href="#" class="dismiss charitable-notif-dismiss-x" aria-label="Dismiss"><svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" width="10" height="10"><path d="M11.8211 1.3415L10.6451 0.166504L5.98305 4.82484L1.32097 0.166504L0.14502 1.3415L4.80711 5.99984L0.14502 10.6582L1.32097 11.8332L5.98305 7.17484L10.6451 11.8332L11.8211 10.6582L7.159 5.99984L11.8211 1.3415Z" fill="currentColor"/></svg></a>' : '';
833 +
834 + if ( 'dashboard' === $location ) {
835 +
836 + // Notification HTML, slightyl different for dashboard.
837 + $active_notifications_html .= sprintf(
838 + '<div aria-expanded="true" class="charitable-notification charitable-notification-%5$s %8$s" data-notification-id="%6$s">
839 + <div>
840 + %7$s
841 + <div class="body">
842 + <div class="title dashboard-title">
843 + <div>%1$s</div>
844 + <div class="date">%3$s</div>
845 + </div>
846 + <div class="notification-content">%2$s</div>
847 + <div class="actions">
848 + %4$s
849 + %9$s
850 + </div>
851 + </div>
852 + </div>
853 + </div>',
854 + esc_html( $title ),
855 + wp_kses( $content, $content_allowed_tags ),
856 + $date_html,
857 + $this->get_notification_buttons_html( $notification ),
858 + esc_attr( $notification['id'] ),
859 + esc_attr( $notification['id'] ),
860 + $this->get_notification_icon_html( $notification ),
861 + esc_attr( $current_class ),
862 + $dismiss_link // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
863 + // $this->get_video_badge_html( $this->get_component_data( $notification['video'] ) )
864 + );
865 +
866 + } else {
867 +
868 + // Notification HTML.
869 + $badge_html = $this->get_notification_badge_html( $notification );
870 + $border_type = ! empty( $notification['badge_type'] ) ? sanitize_html_class( $notification['badge_type'] ) : '';
871 + $border_class = $border_type ? ' charitable-notif-border--' . $border_type : '';
872 +
873 + $active_notifications_html .= sprintf(
874 + '<div aria-expanded="true" class="charitable-notification charitable-notification-%5$s %8$s" data-notification-id="%6$s">
875 + <div class="charitable-notification-card-inner">
876 + %9$s
877 + <div class="notif-left">
878 + %10$s
879 + %7$s
880 + </div>
881 + <div class="body">
882 + %3$s
883 + <h4>%1$s</h4>
884 + <div class="notification-content">%2$s</div>
885 + <div class="actions">
886 + %4$s
887 + </div>
888 + </div>
889 + </div>
890 + </div>',
891 + esc_html( $title ),
892 + wp_kses( $content, $content_allowed_tags ),
893 + $date_html,
894 + $this->get_notification_buttons_html( $notification ),
895 + esc_attr( $notification['id'] ),
896 + esc_attr( $notification['id'] ),
897 + $this->get_notification_icon_html( $notification ),
898 + esc_attr( $current_class ),
899 + $dismiss_link, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
900 + $badge_html // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
901 + );
902 +
903 + }
904 +
905 + ++$active_notifications_count;
906 +
907 + } else {
908 +
909 + // Dismissed notification.
910 + $dismissed_notifications_html .= sprintf(
911 + '<div aria-expanded="true" class="charitable-notification charitable-notification-%5$s %8$s" data-notification-id="%6$s">
912 + <div>
913 + %7$s
914 + <div class="body">
915 + <div class="title">
916 + <div class="date">%3$s</div>
917 + <div>%1$s</div>
918 + </div>
919 + <div class="notification-content">%2$s</div>
920 + <div class="actions">
921 + %4$s
922 + </div>
923 + </div>
924 + </div>
925 + </div>',
926 + esc_html( $title ),
927 + wp_kses( $content, $content_allowed_tags ),
928 + $date_html,
929 + $this->get_notification_buttons_html( $notification ),
930 + esc_attr( $notification['id'] ),
931 + esc_attr( $notification['id'] ),
932 + $this->get_notification_icon_html( $notification ),
933 + esc_attr( $current_class )
934 + // $this->get_video_badge_html( $this->get_component_data( $notification['video'] ) )
935 + );
936 +
937 + ++$dismissed_notifications_count;
938 + }
939 +
632 940 // Only first notification is current.
633 941 $current_class = '';
942 +
943 + if ( 'dashboard' === $location && $active_notifications_count >= $limit_active ) {
944 + break;
945 + }
634 946 }
635 947
948 + // For dashboard, we need to show the actual count being displayed (limited), not the total count
949 + $display_count = $active_notifications_count;
950 + if ( 'dashboard' === $location ) {
951 + // Get the total count for comparison
952 + $total_count = $this->get_new_notifications_count();
953 +
954 + // If we have more than the limit, show the limit with a "+"
955 + if ( $total_count > $limit_active ) {
956 + $display_count = $limit_active . '+';
957 + }
958 + }
959 +
636 960 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
637 961 echo charitable_render(
638 - '/admin/templates/notifications',
962 + $template_location,
639 963 [
640 964 'notifications' => [
641 - 'count' => count( $notifications ),
642 - 'html' => $notifications_html,
965 + 'active_count' => $display_count,
966 + 'dismissed_count' => $dismissed_notifications_count,
967 + 'active_html' => $active_notifications_html,
968 + 'dismissed_html' => $dismissed_notifications_html,
643 969 ],
644 970 ],
645 971 true
646 972 );
973 + }
647 974
975 + /**
976 + * Get notificaiton icon html based on if the type is a success, warning, info.
977 + *
978 + * @since 1.8.3
979 + *
980 + * @param array $notification Notification data.
981 + *
982 + * @return string
983 + */
984 + private function get_notification_icon_html( $notification ) {
985 +
986 + $notification_type = ! empty( $notification['notification_type'] ) ? sanitize_text_field( $notification['notification_type'] ) : '';
987 + $category = ! empty( $notification['category'] ) ? sanitize_text_field( $notification['category'] ) : '';
988 +
989 + // Category-specific icons take priority over generic notification_type icons.
990 + if ( 'celebration' === $notification_type ) {
991 + // Trophy — campaign goal reached.
992 + return '<div class="icon">
993 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-celebration" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
994 + <path d="M8 21h8M12 17v4"/>
995 + <path d="M7 4H5a2 2 0 0 0-2 2v1a4 4 0 0 0 4 4h.5"/>
996 + <path d="M17 4h2a2 2 0 0 1 2 2v1a4 4 0 0 1-4 4h-.5"/>
997 + <path d="M7 4h10v7a5 5 0 0 1-10 0V4z"/>
998 + </svg>
999 + </div>';
1000 + }
1001 +
1002 + if ( 'legal' === $category ) {
1003 + // Same circle exclamation as health alert, orange.
1004 + return '<div class="icon">
1005 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-compliance">
1006 + <path fill-rule="evenodd" clip-rule="evenodd" d="M11.99 2.00012C6.47 2.00012 2 6.48012 2 12.0001C2 17.5201 6.47 22.0001 11.99 22.0001C17.52 22.0001 22 17.5201 22 12.0001C22 6.48012 17.52 2.00012 11.99 2.00012ZM13 13.0001V7.00012H11V13.0001H13ZM13 17.0001V15.0001H11V17.0001H13ZM4 12.0001C4 16.4201 7.58 20.0001 12 20.0001C16.42 20.0001 20 16.4201 20 12.0001C20 7.58012 16.42 4.00012 12 4.00012C7.58 4.00012 4 7.58012 4 12.0001Z" fill="currentColor"></path>
1007 + </svg>
1008 + </div>';
1009 + }
1010 +
1011 + if ( 'milestone' === $category ) {
1012 + // Star — donor milestone.
1013 + return '<div class="icon">
1014 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-milestone" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1015 + <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
1016 + </svg>
1017 + </div>';
1018 + }
1019 +
1020 + if ( 'lifecycle' === $category ) {
1021 + // Flag — campaign live or ended.
1022 + return '<div class="icon">
1023 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-lifecycle" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1024 + <path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/>
1025 + <line x1="4" y1="22" x2="4" y2="15"/>
1026 + </svg>
1027 + </div>';
1028 + }
1029 +
1030 + if ( 'nudge' === $category ) {
1031 + // Lightbulb — engagement nudge.
1032 + return '<div class="icon">
1033 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-nudge" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1034 + <path d="M9 18h6M10 22h4M12 2a7 7 0 00-4 12.87V17h8v-2.13A7 7 0 0012 2z"/>
1035 + </svg>
1036 + </div>';
1037 + }
1038 +
1039 + if ( 'new_feed' === $category ) {
1040 + // Megaphone — announcement.
1041 + return '<div class="icon">
1042 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-icon-announcement" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1043 + <path d="M19 3v18M5 8H3a1 1 0 00-1 1v4a1 1 0 001 1h2l4 3V5L5 8z"/>
1044 + <path d="M19 7l-10 1M19 17L9 16"/>
1045 + </svg>
1046 + </div>';
1047 + }
1048 +
1049 + // Generic fallback by notification_type.
1050 + switch ( $notification_type ) {
1051 + case 'warning':
1052 + // Circle exclamation — health alert (keep as-is).
1053 + $html = '<div class="icon">
1054 + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-circle-exclamation warning">
1055 + <path fill-rule="evenodd" clip-rule="evenodd" d="M11.99 2.00012C6.47 2.00012 2 6.48012 2 12.0001C2 17.5201 6.47 22.0001 11.99 22.0001C17.52 22.0001 22 17.5201 22 12.0001C22 6.48012 17.52 2.00012 11.99 2.00012ZM13 13.0001V7.00012H11V13.0001H13ZM13 17.0001V15.0001H11V17.0001H13ZM4 12.0001C4 16.4201 7.58 20.0001 12 20.0001C16.42 20.0001 20 16.4201 20 12.0001C20 7.58012 16.42 4.00012 12 4.00012C7.58 4.00012 4 7.58012 4 12.0001Z" fill="currentColor"></path>
1056 + </svg>
1057 + </div>';
1058 + break;
1059 + case 'success':
1060 + $html = '<div class="icon"><svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-circle-check success"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path></svg></div>';
1061 + break;
1062 + default:
1063 + $html = '<div class="icon"><svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="charitable-circle-check success"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM10 14.17L16.59 7.58L18 9L10 17L6 13L7.41 11.59L10 14.17Z" fill="currentColor"></path></svg></div>';
1064 + break;
1065 + }
1066 +
1067 + return $html;
648 1068 }
649 1069
650 1070 /**
1071 + * Get badge HTML for a notification card.
1072 + *
1073 + * Badge displays the category label (e.g. "HEALTH ALERT", "MILESTONE").
1074 + * Returns empty string if no badge_label is set.
1075 + *
1076 + * @since 1.8.12
1077 + *
1078 + * @param array $notification Notification data.
1079 + * @return string
1080 + */
1081 + private function get_notification_badge_html( $notification ) {
1082 + if ( empty( $notification['badge_label'] ) ) {
1083 + return '';
1084 + }
1085 +
1086 + $badge_type = ! empty( $notification['badge_type'] ) ? sanitize_html_class( $notification['badge_type'] ) : 'info';
1087 + $label = esc_html( strtoupper( $notification['badge_label'] ) );
1088 +
1089 + return sprintf(
1090 + '<span class="charitable-notif-badge charitable-notif-badge--%s">%s</span>',
1091 + $badge_type,
1092 + $label
1093 + );
1094 + }
1095 +
1096 + /**
1097 + * Get human readable date from MySQL timestamp.
1098 + *
1099 + * @since 1.8.3
1100 + *
1101 + * @param string $mysql_timestamp MySQL timestamp.
1102 + *
1103 + * @return bool
1104 + */
1105 + private function human_readable_date_from_mysql( $mysql_timestamp = false ) {
1106 +
1107 + // Convert the MySQL timestamp to a Unix timestamp.
1108 + $timestamp = strtotime( $mysql_timestamp );
1109 +
1110 + // Get the current time as a Unix timestamp.
1111 + $current_time = current_time( 'timestamp' ); // phpcs:ignore
1112 +
1113 + // Calculate the difference in seconds.
1114 + $time_diff = $current_time - $timestamp;
1115 +
1116 + // 30 days in seconds
1117 + $thirty_days = 30 * 24 * 60 * 60;
1118 +
1119 + if ( $time_diff < $thirty_days ) {
1120 + // If the timestamp is less than 30 days ago, return a 'time ago' format.
1121 + return human_time_diff( $timestamp, $current_time ) . ' ago';
1122 + } else {
1123 + // If the timestamp is 30 days or more in the past, use 'F j, Y' format.
1124 + return date( 'F j, Y', $timestamp ); // phpcs:ignore
1125 + }
1126 + }
1127 +
1128 + /**
651 1129 * Retrieve notification's buttons HTML.
652 1130 *
653 - * @since 1.7.5
1131 + * @since 1.8.3
654 1132 *
655 1133 * @param array $notification Notification data.
656 1134 *
657 1135 * @return string
@@ -690,9 +1168,9 @@
690 1168
691 1169 /**
692 1170 * Retrieve notification's component data by a license type.
693 1171 *
694 - * @since 1.7.5
1172 + * @since 1.8.3
695 1173 *
696 1174 * @param mixed $data Component data.
697 1175 *
698 1176 * @return false|mixed
@@ -714,9 +1192,9 @@
714 1192
715 1193 /**
716 1194 * Retrieve the current installation license type (always lowercase).
717 1195 *
718 - * @since 1.7.5
1196 + * @since 1.8.3
719 1197 *
720 1198 * @return string
721 1199 */
722 1200 private function get_license_type() {
@@ -729,28 +1207,20 @@
729 1207 return 'pro';
730 1208 } else {
731 1209 return 'lite';
732 1210 }
733 -
734 - // $this->license_type = charitable_get_license_type();
735 -
736 - // if ( ! $this->license_type ) {
737 - // $this->license_type = 'lite';
738 - // }
739 -
740 - // return $this->license_type;
741 1211 }
742 1212
743 1213 /**
744 1214 * Dismiss notification via AJAX.
745 1215 *
746 - * @since 1.7.5
1216 + * @since 1.8.3
747 1217 */
748 1218 public function dismiss() {
749 1219
750 1220 // Check for required param, security and access.
751 1221 if (
752 - empty( $_POST['id'] ) ||
1222 + empty( $_POST['notification_id'] ) ||
753 1223 ! check_ajax_referer( 'charitable-admin', 'nonce', false ) ||
754 1224 ! $this->has_access()
755 1225 ) {
756 1226 wp_send_json_error();
@@ -755,9 +1225,9 @@
755 1225 ) {
756 1226 wp_send_json_error();
757 1227 }
758 1228
759 - $id = sanitize_key( $_POST['id'] );
1229 + $id = sanitize_key( $_POST['notification_id'] );
760 1230 $type = is_numeric( $id ) ? 'feed' : 'events';
761 1231 $option = $this->get_option();
762 1232
763 1233 $option['dismissed'][] = $id;
@@ -762,19 +1232,42 @@
762 1232
763 1233 $option['dismissed'][] = $id;
764 1234 $option['dismissed'] = array_unique( $option['dismissed'] );
765 1235
766 - // Remove notification.
767 - if ( is_array( $option[ $type ] ) && ! empty( $option[ $type ] ) ) {
768 - foreach ( $option[ $type ] as $key => $notification ) {
769 - if ( (string) $notification['id'] === (string) $id ) {
770 - unset( $option[ $type ][ $key ] );
1236 + update_option( 'charitable_notifications', $option );
771 1237
772 - break;
773 - }
774 - }
1238 + wp_send_json_success();
1239 + }
1240 +
1241 + /**
1242 + * Dismiss multiple notifications via AJAX.
1243 + *
1244 + * @since 1.8.3
1245 + */
1246 + public function dismiss_multiple() {
1247 +
1248 + // Check for required param, security and access.
1249 + if (
1250 + empty( $_POST['notification_ids'] ) ||
1251 + ! check_ajax_referer( 'charitable-admin', 'nonce', false ) ||
1252 + ! $this->has_access()
1253 + ) {
1254 + wp_send_json_error();
775 1255 }
776 1256
1257 + if ( ! is_array( $_POST['notification_ids'] ) ) {
1258 + wp_send_json_error();
1259 + }
1260 +
1261 + $option = $this->get_option();
1262 + $notification_ids = array_map( 'sanitize_key', $_POST['notification_ids'] );
1263 + foreach ( $notification_ids as $id ) {
1264 + $type = is_numeric( $id ) ? 'feed' : 'events';
1265 + $option['dismissed'][] = $id;
1266 + $option['dismissed'] = array_unique( $option['dismissed'] );
1267 +
1268 + }
1269 +
777 1270 update_option( 'charitable_notifications', $option );
778 1271
779 1272 wp_send_json_success();
780 1273 }
@@ -781,9 +1274,9 @@
781 1274
782 1275 /**
783 1276 * Prepare button URL.
784 1277 *
785 - * @since 1.7.5
1278 + * @since 1.8.3
786 1279 *
787 1280 * @param array $btn Button data.
788 1281 *
789 1282 * @return string
@@ -803,9 +1296,9 @@
803 1296
804 1297 /**
805 1298 * Get the notification's video badge HTML.
806 1299 *
807 - * @since 1.7.5
1300 + * @since 1.8.3
808 1301 *
809 1302 * @param string $video_url Valid video URL.
810 1303 *
811 1304 * @return string
@@ -820,12 +1313,12 @@
820 1313
821 1314 $data_attr_lity = wp_is_mobile() ? '' : 'data-lity';
822 1315
823 1316 return sprintf(
824 - '<a class="charitable-notifications-badge" href="%1$s" %2$s><i class="fa fa-youtube-play" aria-hidden="true"></i>%3$s</a>',
1317 + '<a class="charitable-notifications-badge" href="%1$s" %2$s>%3$s</a>',
825 1318 esc_url( $video_url ),
826 1319 esc_attr( $data_attr_lity ),
827 - esc_html__( 'Watch Video', 'charitable-lite' )
1320 + esc_html__( 'Watch Video', 'charitable' )
828 1321 );
829 1322 }
830 1323
831 1324 /**
@@ -835,16 +1328,31 @@
835 1328 *
836 1329 * @return boolean
837 1330 */
838 1331 public function show_notifications() {
839 - if ( isset( $_GET['taxonomy'] ) ) {
1332 + if ( isset( $_GET['taxonomy'] ) ) { // phpcs:ignore
840 1333 return false;
841 1334 }
842 - if ( isset( $_GET['post_type'] ) && $_GET['post_type'] == 'campaign' ) {
1335 + if ( isset( $_GET['post_type'] ) && sanitize_text_field( $_GET['post_type'] ) === 'campaign' ) { // phpcs:ignore
843 1336 return true;
844 1337 }
845 - if ( isset( $_GET['post_type'] ) && $_GET['post_type'] == 'donation' ) {
1338 + if ( isset( $_GET['post_type'] ) && sanitize_text_field( $_GET['post_type'] ) === 'donation' ) { // phpcs:ignore
846 1339 return true;
847 1340 }
848 1341 return false;
1342 + }
1343 +
1344 + /**
1345 + * This will incercept the request and redirect to the dashboard page.
1346 + *
1347 + * @since 1.8.3
1348 + * @version 1.8.3.1
1349 + */
1350 + public function maybe_redirect_from_notifications() {
1351 +
1352 + if ( ! empty( $_GET['page'] ) && 'charitable-notifications' === $_GET['page'] ) { // phpcs:ignore
1353 + delete_transient( 'charitable_autoshow_plugin_notifications' );
1354 + wp_safe_redirect( admin_url( 'admin.php?page=charitable-dashboard' ) );
1355 + exit;
1356 + }
849 1357 }
850 1358 }