PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13.1
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13.1
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
charitable / includes / admin / class-charitable-external-banner.php

class-charitable-external-banner.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13.1, at includes/admin/class-charitable-external-banner.php

496 lines 13.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Displays Charitable promotional banners on non-Charitable admin screens.
4 *
5 * @package Charitable/Classes/Charitable_External_Banner
6 * @author David Bisset
7 * @copyright Copyright (c) 2023, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.8.10
10 */
11
12 // Exit if accessed directly.
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 if ( ! class_exists( 'Charitable_External_Banner' ) ) :
18
19 /**
20 * Charitable_External_Banner
21 *
22 * Renders Charitable banners on third-party plugin admin screens
23 * when conditions are met (e.g., GiveWP is active).
24 *
25 * @since 1.8.10
26 */
27 class Charitable_External_Banner {
28
29 /**
30 * The single instance of this class.
31 *
32 * @var Charitable_External_Banner|null
33 */
34 private static $instance = null;
35
36 /**
37 * Option key for storing dismissed banner IDs.
38 *
39 * @var string
40 */
41 const DISMISSED_OPTION = 'charitable_external_banners_dismissed';
42
43 /**
44 * Create class object.
45 *
46 * @since 1.8.10
47 */
48 public function __construct() {
49 add_action( 'admin_notices', array( $this, 'maybe_render_banner' ) );
50 add_action( 'admin_enqueue_scripts', array( $this, 'maybe_enqueue_assets' ) );
51 add_action( 'wp_ajax_charitable_dismiss_external_banner', array( $this, 'ajax_dismiss_banner' ) );
52 add_action( 'charitable_after_clear_expired_options', array( $this, 'reset_dismissed_banners' ) );
53 }
54
55 /**
56 * Get the banner configurations for external screens.
57 *
58 * Each banner config includes:
59 * - 'id' (string) Unique banner identifier.
60 * - 'screens' (array) WordPress screen IDs where the banner should appear.
61 * - 'condition' (callable) Optional. Must return true for the banner to display.
62 * - 'messages' (array) Screen-specific messages. Keys are screen IDs, 'default' is fallback.
63 * - 'button_url' (string) The CTA link URL.
64 * - 'button_text' (string) The CTA link text.
65 *
66 * @since 1.8.10
67 *
68 * @return array
69 */
70 private function get_banner_configs() {
71 $configs = array();
72
73 // GiveWP banner — show on GiveWP admin screens when GiveWP is active.
74 if ( $this->is_givewp_active() ) {
75 $counts = $this->get_givewp_counts();
76
77 $configs[] = array(
78 'id' => 'givewp_import',
79 'screens' => array(
80 'give_forms_page_give-campaigns',
81 'give_forms_page_give-payment-history',
82 'give_forms_page_give-donors',
83 'give_forms_page_give-reports',
84 'give_forms_page_give-tools',
85 'give_forms_page_give-settings',
86 ),
87 'messages' => $this->get_givewp_messages( $counts ),
88 'button_url' => admin_url( 'admin.php?page=charitable-tools&tab=import' ),
89 'button_text' => __( 'Import Now', 'charitable' ),
90 );
91 }
92
93 /**
94 * Filter the external banner configurations.
95 *
96 * @since 1.8.10
97 *
98 * @param array $configs Banner configurations.
99 */
100 return apply_filters( 'charitable_external_banner_configs', $configs );
101 }
102
103 /**
104 * Maybe render a banner on the current screen.
105 *
106 * @since 1.8.10
107 *
108 * @return void
109 */
110 public function maybe_render_banner() {
111 $screen = get_current_screen();
112
113 if ( is_null( $screen ) ) {
114 return;
115 }
116
117 $dismissed = get_option( self::DISMISSED_OPTION, array() );
118 $configs = $this->get_banner_configs();
119
120 foreach ( $configs as $config ) {
121 // Skip if not on a target screen.
122 if ( ! in_array( $screen->id, $config['screens'], true ) ) {
123 continue;
124 }
125
126 // Skip if dismissed.
127 if ( isset( $dismissed[ $config['id'] ] ) ) {
128 continue;
129 }
130
131 // Get the screen-specific or default message.
132 $message = isset( $config['messages'][ $screen->id ] )
133 ? $config['messages'][ $screen->id ]
134 : $config['messages']['default'];
135
136 $this->render_banner( $config, $message );
137
138 // Only render one banner per page.
139 break;
140 }
141 }
142
143 /**
144 * Render a single external banner.
145 *
146 * @since 1.8.10
147 *
148 * @param array $config The banner configuration.
149 * @param string $message The message to display.
150 *
151 * @return void
152 */
153 private function render_banner( $config, $message ) {
154 $nonce = wp_create_nonce( 'charitable_external_banner_dismiss' );
155
156 printf(
157 '<div class="charitable-external-banner" data-banner-id="%s" data-nonce="%s" style="display:none;">
158 <div class="charitable-external-banner-content">
159 <span class="charitable-external-banner-icon">
160 <img src="%s" alt="%s" width="20" height="20" />
161 </span>
162 <span class="charitable-external-banner-message">%s</span>
163 <a href="%s" class="charitable-external-banner-cta">%s &rarr;</a>
164 </div>
165 <button type="button" class="charitable-external-banner-dismiss" aria-label="%s">&times;</button>
166 </div>',
167 esc_attr( $config['id'] ),
168 esc_attr( $nonce ),
169 esc_url( charitable()->get_path( 'assets', false ) . 'images/charitable-logo.svg' ),
170 esc_attr__( 'Charitable', 'charitable' ),
171 wp_kses_post( $message ),
172 esc_url( $config['button_url'] ),
173 esc_html( $config['button_text'] ),
174 esc_attr__( 'Dismiss this notice', 'charitable' )
175 );
176 }
177
178 /**
179 * Enqueue banner assets on target screens.
180 *
181 * @since 1.8.10
182 *
183 * @return void
184 */
185 public function maybe_enqueue_assets() {
186 $screen = get_current_screen();
187
188 if ( is_null( $screen ) ) {
189 return;
190 }
191
192 $dismissed = get_option( self::DISMISSED_OPTION, array() );
193 $configs = $this->get_banner_configs();
194
195 foreach ( $configs as $config ) {
196 if ( in_array( $screen->id, $config['screens'], true ) && ! isset( $dismissed[ $config['id'] ] ) ) {
197 $this->enqueue_assets();
198 return;
199 }
200 }
201 }
202
203 /**
204 * Enqueue the banner JS and inline CSS.
205 *
206 * @since 1.8.10
207 *
208 * @return void
209 */
210 private function enqueue_assets() {
211 $suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
212
213 wp_enqueue_script(
214 'charitable-external-banner',
215 charitable()->get_path( 'assets', false ) . 'js/admin/charitable-external-banner' . $suffix . '.js',
216 array( 'jquery' ),
217 charitable()->get_version(),
218 true
219 );
220
221 wp_localize_script(
222 'charitable-external-banner',
223 'charitable_external_banner',
224 array(
225 'ajaxurl' => admin_url( 'admin-ajax.php' ),
226 )
227 );
228
229 // Inline CSS so we don't need a separate CSS file for this.
230 wp_add_inline_style( 'wp-admin', $this->get_inline_css() );
231 }
232
233 /**
234 * Get the inline CSS for the banner.
235 *
236 * @since 1.8.10
237 *
238 * @return string
239 */
240 private function get_inline_css() {
241 return '
242 .charitable-external-banner {
243 background: #fc9a2a;
244 color: #fff;
245 padding: 11px 40px 11px 20px;
246 font-size: 13px;
247 line-height: 1.5;
248 position: relative;
249 margin-left: -20px;
250 margin-right: 0;
251 }
252 .charitable-external-banner-content {
253 display: flex;
254 align-items: center;
255 justify-content: center;
256 gap: 8px;
257 flex-wrap: wrap;
258 }
259 .charitable-external-banner-icon {
260 display: inline-flex;
261 align-items: center;
262 flex-shrink: 0;
263 }
264 .charitable-external-banner-icon img {
265 display: block;
266 }
267 .charitable-external-banner-message {
268 color: #fff;
269 }
270 .charitable-external-banner-message strong {
271 font-weight: 700;
272 }
273 .charitable-external-banner-cta {
274 color: #fff !important;
275 font-weight: 700;
276 text-decoration: underline;
277 white-space: nowrap;
278 }
279 .charitable-external-banner-cta:hover {
280 color: #fff !important;
281 opacity: 0.85;
282 }
283 .charitable-external-banner-dismiss {
284 position: absolute;
285 top: 50%;
286 right: 10px;
287 transform: translateY(-50%);
288 background: rgba(0, 0, 0, 0.15);
289 border: none;
290 border-radius: 50%;
291 color: #fff;
292 font-size: 18px;
293 font-weight: 700;
294 cursor: pointer;
295 width: 24px;
296 height: 24px;
297 padding: 0;
298 line-height: 22px;
299 text-align: center;
300 opacity: 1;
301 }
302 .charitable-external-banner-dismiss:hover {
303 background: rgba(0, 0, 0, 0.3);
304 }
305 @media screen and (max-width: 782px) {
306 .charitable-external-banner {
307 padding: 10px 36px 10px 12px;
308 }
309 .charitable-external-banner-content {
310 justify-content: flex-start;
311 }
312 }
313 ';
314 }
315
316 /**
317 * AJAX handler for dismissing an external banner.
318 *
319 * @since 1.8.10
320 *
321 * @return void
322 */
323 public function ajax_dismiss_banner() {
324 check_ajax_referer( 'charitable_external_banner_dismiss', 'nonce' );
325
326 if ( ! current_user_can( 'manage_options' ) ) {
327 wp_send_json_error( 'Insufficient permissions.' );
328 }
329
330 $banner_id = isset( $_POST['banner_id'] ) ? sanitize_text_field( wp_unslash( $_POST['banner_id'] ) ) : '';
331
332 if ( empty( $banner_id ) ) {
333 wp_send_json_error( 'Missing banner ID.' );
334 }
335
336 $dismissed = get_option( self::DISMISSED_OPTION, array() );
337 $dismissed[ $banner_id ] = time();
338 update_option( self::DISMISSED_OPTION, $dismissed );
339
340 wp_send_json_success();
341 }
342
343 /**
344 * Reset dismissed banners when Charitable cache is cleared.
345 *
346 * Hooked to 'charitable_after_clear_expired_options'.
347 *
348 * @since 1.8.10
349 *
350 * @return void
351 */
352 public function reset_dismissed_banners() {
353 delete_option( self::DISMISSED_OPTION );
354 }
355
356 /**
357 * Check if GiveWP is active.
358 *
359 * @since 1.8.10
360 *
361 * @return bool
362 */
363 private function is_givewp_active() {
364 return defined( 'GIVE_VERSION' ) || class_exists( 'Give' );
365 }
366
367 /**
368 * Get GiveWP data counts for dynamic messaging.
369 *
370 * @since 1.8.10
371 *
372 * @return array {
373 * @type int $campaigns Number of GiveWP campaigns.
374 * @type int $forms Number of GiveWP donation forms.
375 * @type int $donations Number of GiveWP donations.
376 * @type int $donors Number of GiveWP donors.
377 * }
378 */
379 private function get_givewp_counts() {
380 $cached = get_transient( 'charitable_givewp_counts' );
381
382 if ( false !== $cached ) {
383 return $cached;
384 }
385
386 global $wpdb;
387
388 $counts = array(
389 'campaigns' => 0,
390 'forms' => 0,
391 'donations' => 0,
392 'donors' => 0,
393 );
394
395 // Count forms (give_forms post type).
396 $counts['forms'] = (int) $wpdb->get_var(
397 $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status IN ('publish', 'draft', 'private')", 'give_forms' )
398 );
399
400 // Count campaigns (give_campaigns custom table, if it exists).
401 $campaigns_table = $wpdb->prefix . 'give_campaigns';
402 if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $campaigns_table ) ) === $campaigns_table ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery
403 $counts['campaigns'] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$campaigns_table} WHERE %d = %d", 1, 1 ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
404 }
405
406 // Use the higher of campaigns or forms for "campaigns" messaging.
407 $counts['campaigns'] = max( $counts['campaigns'], $counts['forms'] );
408
409 // Count donations (give_payment post type).
410 $counts['donations'] = (int) $wpdb->get_var(
411 $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status != %s", 'give_payment', 'trash' )
412 );
413
414 // Count donors (give_donors custom table).
415 $donors_table = $wpdb->prefix . 'give_donors';
416 if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $donors_table ) ) === $donors_table ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery
417 $counts['donors'] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$donors_table} WHERE %d = %d", 1, 1 ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
418 }
419
420 set_transient( 'charitable_givewp_counts', $counts, HOUR_IN_SECONDS );
421
422 return $counts;
423 }
424
425 /**
426 * Get screen-specific messages for GiveWP pages.
427 *
428 * @since 1.8.10
429 *
430 * @param array $counts GiveWP data counts.
431 *
432 * @return array Keyed by screen ID, with 'default' fallback.
433 */
434 private function get_givewp_messages( $counts ) {
435 $messages = array();
436
437 $generic = __( 'You have Charitable installed! Import your GiveWP campaigns, donations, and donors into Charitable.', 'charitable' );
438
439 // Campaigns page.
440 if ( $counts['campaigns'] > 0 ) {
441 $messages['give_forms_page_give-campaigns'] = sprintf(
442 /* translators: %s: number of campaigns */
443 __( 'You have <strong>%s campaigns</strong> in GiveWP. Import them into Charitable in just a few clicks.', 'charitable' ),
444 number_format_i18n( $counts['campaigns'] )
445 );
446 }
447
448 // Donations page.
449 if ( $counts['donations'] > 0 ) {
450 $messages['give_forms_page_give-payment-history'] = sprintf(
451 /* translators: %s: number of donations */
452 __( 'You have <strong>%s donations</strong> in GiveWP. Import them into Charitable in just a few clicks.', 'charitable' ),
453 number_format_i18n( $counts['donations'] )
454 );
455 }
456
457 // Donors page.
458 if ( $counts['donors'] > 0 ) {
459 $messages['give_forms_page_give-donors'] = sprintf(
460 /* translators: %s: number of donors */
461 __( 'You have <strong>%s donors</strong> in GiveWP. Bring them into Charitable and keep your donor relationships intact.', 'charitable' ),
462 number_format_i18n( $counts['donors'] )
463 );
464 }
465
466 // Reports page — always a custom message.
467 $messages['give_forms_page_give-reports'] = __( 'Get better reporting with Charitable. Import your GiveWP data for unified analytics.', 'charitable' );
468
469 // Settings and Tools — always a custom message.
470 $messages['give_forms_page_give-settings'] = __( 'Already using Charitable? Import your GiveWP data &mdash; campaigns, donations, and donors.', 'charitable' );
471 $messages['give_forms_page_give-tools'] = $messages['give_forms_page_give-settings'];
472
473 // Default fallback for any screen without a specific message.
474 $messages['default'] = $generic;
475
476 return $messages;
477 }
478
479 /**
480 * Create and return the class object.
481 *
482 * @since 1.8.10
483 *
484 * @return Charitable_External_Banner
485 */
486 public static function get_instance() {
487 if ( is_null( self::$instance ) ) {
488 self::$instance = new self();
489 }
490
491 return self::$instance;
492 }
493 }
494
495 endif;
496