PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.2.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.2.0
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
suredonation / inc / admin / notices.php

notices.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.2.0, at inc/admin/notices.php

535 lines 16.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin Notices.
4 *
5 * Registers SureDonation's engagement admin notices:
6 *
7 * - Review notice (donation) : shown once the site has at least one live
8 * donation, after the 3-day install grace.
9 * - Review notice (gateway) : shown when a payment gateway is connected but
10 * no live donation exists yet, after the 3-day
11 * install grace.
12 * - Setup gateway notice : shown instantly (no grace) when no payment
13 * gateway is connected at all.
14 *
15 * The three notices are mutually exclusive by construction. Priority is:
16 * live donation > gateway configured > no gateway.
17 *
18 * @package SureDonation
19 */
20
21 namespace SureDonation\Inc\Admin;
22
23 use SureDonation\Inc\Database\Tables\Donations;
24 use SureDonation\Inc\Helper;
25 use SureDonation\Inc\Payments\PayPal\PayPal_Helper;
26 use SureDonation\Inc\Payments\Stripe\Stripe_Helper;
27
28 // Exit if accessed directly.
29 if ( ! defined( 'ABSPATH' ) ) {
30 exit;
31 }
32
33 /**
34 * Admin Notices class.
35 *
36 * @since 1.2.0
37 */
38 class Notices {
39
40 /**
41 * Install-grace period, in seconds, before the review notices may appear.
42 *
43 * @var int
44 * @since 1.2.0
45 */
46 public const REVIEW_NOTICE_DELAY = 3 * DAY_IN_SECONDS;
47
48 /**
49 * WordPress.org review URL for the CTA.
50 *
51 * @var string
52 * @since 1.2.0
53 */
54 public const REVIEW_URL = 'https://wordpress.org/support/plugin/suredonation/reviews/#new-post';
55
56 /**
57 * Instance of this class.
58 *
59 * @var Notices|null
60 * @since 1.2.0
61 */
62 private static $instance = null;
63
64 /**
65 * Memoized "has at least one live donation" result.
66 *
67 * @var bool|null
68 * @since 1.2.0
69 */
70 private $has_live_donation = null;
71
72 /**
73 * Memoized "a payment gateway is configured" result.
74 *
75 * @var bool|null
76 * @since 1.2.0
77 */
78 private $gateway_configured = null;
79
80 /**
81 * Constructor.
82 *
83 * @since 1.2.0
84 */
85 private function __construct() {
86 // Ensure the notices library (and its priority-30 renderer) is loaded
87 // early, before the admin_notices hook fires.
88 if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
89 require_once SUREDONATION_DIR . 'inc/lib/astra-notices/class-bsf-admin-notices.php';
90 }
91
92 add_action( 'admin_notices', [ $this, 'display_review_notice_donation' ] );
93 add_action( 'admin_notices', [ $this, 'display_review_notice_gateway' ] );
94 add_action( 'admin_notices', [ $this, 'display_setup_gateway_notice' ] );
95
96 // Load the setup-notice styles from the admin <head> (not the late
97 // after-markup hook) so the banner never renders unstyled first.
98 add_action( 'admin_enqueue_scripts', [ $this, 'maybe_enqueue_setup_notice_style' ] );
99
100 add_action( 'wp_ajax_suredonation_notice_response', [ $this, 'handle_notice_response' ] );
101 }
102
103 /**
104 * Get instance of this class.
105 *
106 * @return Notices
107 * @since 1.2.0
108 */
109 public static function get_instance() {
110 if ( null === self::$instance ) {
111 self::$instance = new self();
112 }
113 return self::$instance;
114 }
115
116 /**
117 * Review notice shown after the first live donation (notice A).
118 *
119 * @return void
120 * @since 1.2.0
121 */
122 public function display_review_notice_donation() {
123 if ( ! Helper::current_user_can() ) {
124 return;
125 }
126
127 if ( ! apply_filters( 'suredonation_show_review_notice_donation', true ) ) {
128 return;
129 }
130
131 if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
132 return;
133 }
134
135 \BSF_Admin_Notices::add_notice(
136 [
137 'id' => 'sd-review-donation',
138 'type' => '',
139 'message' => $this->build_notice_markup(
140 esc_html__( 'You received your first donation with SureDonation!', 'suredonation' ),
141 esc_html__( 'That is a big milestone. If SureDonation is helping power your cause, would you take a moment to leave a 5-star review on WordPress.org? It really helps.', 'suredonation' ),
142 esc_url( self::REVIEW_URL ),
143 esc_html__( 'Rate SureDonation', 'suredonation' ),
144 esc_html__( 'Maybe later', 'suredonation' ),
145 esc_html__( 'I already did', 'suredonation' ),
146 WEEK_IN_SECONDS,
147 true
148 ),
149 'repeat-notice-after' => WEEK_IN_SECONDS,
150 'show_if' => $this->is_three_days_elapsed() && $this->has_live_donation(),
151 'display-with-other-notices' => true,
152 ]
153 );
154
155 add_action( 'astra_notice_after_markup_sd-review-donation', [ $this, 'enqueue_notice_response_script' ] );
156 }
157
158 /**
159 * Review notice shown when a gateway is configured but there are no live
160 * donations yet (notice B).
161 *
162 * @return void
163 * @since 1.2.0
164 */
165 public function display_review_notice_gateway() {
166 if ( ! Helper::current_user_can() ) {
167 return;
168 }
169
170 if ( ! apply_filters( 'suredonation_show_review_notice_gateway', true ) ) {
171 return;
172 }
173
174 if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
175 return;
176 }
177
178 \BSF_Admin_Notices::add_notice(
179 [
180 'id' => 'sd-review-gateway',
181 'type' => '',
182 'message' => $this->build_notice_markup(
183 esc_html__( 'Your payment gateway is all set up!', 'suredonation' ),
184 esc_html__( 'You have connected a payment gateway and SureDonation is ready to start raising funds. If you are enjoying it so far, a quick 5-star review on WordPress.org would mean a lot.', 'suredonation' ),
185 esc_url( self::REVIEW_URL ),
186 esc_html__( 'Rate SureDonation', 'suredonation' ),
187 esc_html__( 'Maybe later', 'suredonation' ),
188 esc_html__( 'I already did', 'suredonation' ),
189 WEEK_IN_SECONDS,
190 true
191 ),
192 'repeat-notice-after' => WEEK_IN_SECONDS,
193 'show_if' => $this->is_three_days_elapsed() && ! $this->has_live_donation() && $this->is_gateway_configured(),
194 'display-with-other-notices' => true,
195 ]
196 );
197
198 add_action( 'astra_notice_after_markup_sd-review-gateway', [ $this, 'enqueue_notice_response_script' ] );
199 }
200
201 /**
202 * Setup notice shown instantly when no payment gateway is connected
203 * (notice C). No install-grace applies to this notice.
204 *
205 * @return void
206 * @since 1.2.0
207 */
208 public function display_setup_gateway_notice() {
209 if ( ! Helper::current_user_can() ) {
210 return;
211 }
212
213 if ( ! apply_filters( 'suredonation_show_setup_gateway_notice', true ) ) {
214 return;
215 }
216
217 if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
218 return;
219 }
220
221 \BSF_Admin_Notices::add_notice(
222 [
223 'id' => 'sd-setup-gateway',
224 'type' => '',
225 'message' => $this->build_setup_notice_markup(),
226 'repeat-notice-after' => WEEK_IN_SECONDS,
227 'show_if' => ! $this->has_live_donation() && ! $this->is_gateway_configured(),
228 'display-with-other-notices' => true,
229 ]
230 );
231
232 add_action( 'astra_notice_after_markup_sd-setup-gateway', [ $this, 'enqueue_notice_response_script' ] );
233 }
234
235 /**
236 * Enqueue the notice-response analytics script.
237 *
238 * Called via the astra_notice_after_markup_{id} hook so the script only
239 * loads when a SureDonation notice is actually rendered.
240 *
241 * @return void
242 * @since 1.2.0
243 */
244 public function enqueue_notice_response_script() {
245 if ( wp_script_is( 'suredonation-notice-response', 'enqueued' ) ) {
246 return;
247 }
248
249 wp_enqueue_script(
250 'suredonation-notice-response',
251 SUREDONATION_URL . 'assets/js/notice-response.js',
252 [],
253 SUREDONATION_VER,
254 true
255 );
256
257 wp_localize_script(
258 'suredonation-notice-response',
259 'suredonationNoticeResponse',
260 [
261 'ajaxurl' => admin_url( 'admin-ajax.php' ),
262 'nonce' => wp_create_nonce( 'suredonation_notice_response' ),
263 ]
264 );
265 }
266
267 /**
268 * Handle the notice-response AJAX request.
269 *
270 * Validates the request and records the analytics event for the notice
271 * button that was clicked.
272 *
273 * @return void
274 * @since 1.2.0
275 */
276 public function handle_notice_response() {
277 if ( ! check_ajax_referer( 'suredonation_notice_response', 'nonce', false ) ) {
278 wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'suredonation' ) ], 403 );
279 }
280
281 if ( ! Helper::current_user_can() ) {
282 wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'suredonation' ) ], 403 );
283 }
284
285 $notice_id = isset( $_POST['notice_id'] ) ? sanitize_text_field( wp_unslash( $_POST['notice_id'] ) ) : '';
286 $button = isset( $_POST['button'] ) ? sanitize_text_field( wp_unslash( $_POST['button'] ) ) : '';
287
288 $valid = [
289 'sd-review-donation' => [
290 'rate_suredonation' => 'review_notice_donation_cta',
291 'maybe_later' => 'review_notice_donation_snooze',
292 'dismissed' => 'review_notice_donation_dismiss',
293 ],
294 'sd-review-gateway' => [
295 'rate_suredonation' => 'review_notice_gateway_cta',
296 'maybe_later' => 'review_notice_gateway_snooze',
297 'dismissed' => 'review_notice_gateway_dismiss',
298 ],
299 'sd-setup-gateway' => [
300 'configure_gateway' => 'setup_gateway_notice_cta',
301 'maybe_later' => 'setup_gateway_notice_snooze',
302 'dismissed' => 'setup_gateway_notice_dismiss',
303 ],
304 ];
305
306 if ( ! isset( $valid[ $notice_id ][ $button ] ) ) {
307 wp_send_json_error( [ 'message' => __( 'Invalid parameters.', 'suredonation' ) ], 400 );
308 }
309
310 $event_name = $valid[ $notice_id ][ $button ];
311
312 $events = Analytics::events();
313 if ( null !== $events ) {
314 $events->track( $event_name, $button );
315 }
316
317 wp_send_json_success();
318 }
319
320 /**
321 * Build the shared HTML markup for admin notices.
322 *
323 * All text parameters must be pre-escaped by the caller (e.g. via
324 * esc_html__()). URL parameters must be pre-escaped via esc_url().
325 *
326 * @param string $heading The notice heading text (pre-escaped).
327 * @param string $message The notice body text (pre-escaped).
328 * @param string $cta_url The primary CTA URL (pre-escaped).
329 * @param string $cta_text The primary CTA button text (pre-escaped).
330 * @param string $snooze_text The snooze button text (pre-escaped).
331 * @param string $dismiss_text The dismiss button text (pre-escaped).
332 * @param int $snooze_duration Snooze duration in seconds for the data-repeat-notice-after attribute.
333 * @param bool $external_cta Whether the CTA opens in a new tab and also dismisses the notice
334 * via the astra-notice-close class. Default false.
335 * @return string The notice HTML markup.
336 * @since 1.2.0
337 */
338 private function build_notice_markup( $heading, $message, $cta_url, $cta_text, $snooze_text, $dismiss_text, $snooze_duration, $external_cta = false ) {
339 $image_path = esc_url( SUREDONATION_URL . 'images/suredonation-icon.svg' );
340 $cta_class = $external_cta ? 'astra-notice-close button-primary' : 'button-primary';
341 $cta_attrs = $external_cta ? ' target="_blank" rel="noopener noreferrer"' : '';
342
343 return sprintf(
344 '<div class="notice-image">
345 <img src="%1$s" class="custom-logo" alt="SureDonation" width="64" height="64" itemprop="logo">
346 </div>
347 <div class="notice-content">
348 <div class="notice-heading">
349 %2$s
350 </div>
351 %3$s<br />
352 <div class="astra-review-notice-container">
353 <a href="%4$s" class="%5$s"%6$s>
354 %7$s
355 </a>
356 <span class="dashicons dashicons-clock" aria-hidden="true"></span>
357 <a href="#" data-repeat-notice-after="%8$s" class="astra-notice-close">
358 %9$s
359 </a>
360 <span class="dashicons dashicons-smiley" aria-hidden="true"></span>
361 <a href="#" class="astra-notice-close">
362 %10$s
363 </a>
364 </div>
365 </div>',
366 $image_path,
367 $heading,
368 $message,
369 $cta_url,
370 esc_attr( $cta_class ),
371 $cta_attrs,
372 $cta_text,
373 $snooze_duration,
374 $snooze_text,
375 $dismiss_text
376 );
377 }
378
379 /**
380 * Build the markup for the "configure a payment gateway" setup notice
381 * (notice C).
382 *
383 * This notice uses a dedicated banner layout (accent bar, icon, heading,
384 * body, primary CTA and a right-side illustration) styled via
385 * setup-gateway-notice.css, rather than the shared review-notice markup.
386 *
387 * @return string The notice HTML markup.
388 * @since 1.2.0
389 */
390 private function build_setup_notice_markup() {
391 return sprintf(
392 '<div class="sd-setup-notice">
393 <div class="sd-setup-notice__main">
394 <img class="sd-setup-notice__icon" src="%5$s" alt="" width="28" height="28" />
395 <div class="sd-setup-notice__body">
396 <h2 class="sd-setup-notice__title">%1$s</h2>
397 <p class="sd-setup-notice__text">%2$s</p>
398 <a href="%3$s" class="button button-primary sd-setup-notice__button">%4$s</a>
399 </div>
400 </div>
401 <img class="sd-setup-notice__art" src="%6$s" alt="" width="187" height="128" />
402 </div>',
403 esc_html__( 'Your donation site is almost ready!', 'suredonation' ),
404 esc_html__( 'Connect a payment gateway to start accepting donations. Set up Stripe or PayPal in just a few clicks to go live.', 'suredonation' ),
405 esc_url( admin_url( 'admin.php?page=suredonation#/settings?tab=payments&subpage=stripe' ) ),
406 esc_html__( 'Configure Payment Gateway', 'suredonation' ),
407 esc_url( SUREDONATION_URL . 'images/suredonation-icon.svg' ),
408 esc_url( SUREDONATION_URL . 'images/payment-gateway-notice.png' )
409 );
410 }
411
412 /**
413 * Enqueue the setup-notice stylesheet.
414 *
415 * @return void
416 * @since 1.2.0
417 */
418 public function enqueue_setup_notice_style() {
419 if ( wp_style_is( 'suredonation-setup-notice', 'enqueued' ) ) {
420 return;
421 }
422
423 wp_enqueue_style(
424 'suredonation-setup-notice',
425 SUREDONATION_URL . 'assets/css/setup-gateway-notice.css',
426 [],
427 SUREDONATION_VER
428 );
429 }
430
431 /**
432 * Enqueue the setup-notice stylesheet from the admin <head> when the setup
433 * notice is eligible to show.
434 *
435 * Hooked on admin_enqueue_scripts (which runs before admin_head) and gated
436 * by the same conditions as the notice itself, so the stylesheet is in the
437 * page head before the banner paints. This avoids the flash of unstyled
438 * content that occurred when the CSS was enqueued on the notice's
439 * after-markup hook (which fires at admin_notices priority 30, after styles
440 * have already been printed).
441 *
442 * @return void
443 * @since 1.2.0
444 */
445 public function maybe_enqueue_setup_notice_style() {
446 if ( ! Helper::current_user_can() ) {
447 return;
448 }
449
450 if ( ! apply_filters( 'suredonation_show_setup_gateway_notice', true ) ) {
451 return;
452 }
453
454 if ( $this->has_live_donation() || $this->is_gateway_configured() ) {
455 return;
456 }
457
458 $this->enqueue_setup_notice_style();
459 }
460
461 /**
462 * Whether the 3-day install grace has elapsed.
463 *
464 * @return bool
465 * @since 1.2.0
466 */
467 private function is_three_days_elapsed() {
468 return ( time() - $this->get_install_time() ) >= self::REVIEW_NOTICE_DELAY;
469 }
470
471 /**
472 * Get (creating if missing) the plugin install timestamp.
473 *
474 * The activation hook seeds this on fresh installs; this getter back-fills
475 * it for sites that were already active before the option existed, so their
476 * grace period starts from the first admin pageload after the update.
477 *
478 * @return int Unix timestamp.
479 * @since 1.2.0
480 */
481 private function get_install_time() {
482 $install_time = Helper::get_integer_value( get_option( 'suredonation_install_time', 0 ) );
483
484 if ( ! $install_time ) {
485 $install_time = time();
486 update_option( 'suredonation_install_time', $install_time );
487 }
488
489 return $install_time;
490 }
491
492 /**
493 * Whether the site has at least one completed, live-mode donation.
494 *
495 * @return bool
496 * @since 1.2.0
497 */
498 private function has_live_donation() {
499 if ( null === $this->has_live_donation ) {
500 // Persist a monotonic flag: once the site has recorded a completed
501 // live donation it stays "true" for this notice's purpose, so we
502 // stop running COUNT(*) on every admin pageload once it is set.
503 if ( get_option( 'suredonation_has_live_donation' ) ) {
504 $this->has_live_donation = true;
505 } else {
506 $this->has_live_donation = Donations::count_live_completed() >= 1;
507
508 if ( $this->has_live_donation ) {
509 update_option( 'suredonation_has_live_donation', 1, false );
510 }
511 }
512 }
513
514 return $this->has_live_donation;
515 }
516
517 /**
518 * Whether a payment gateway (Stripe or PayPal) is connected, in any mode.
519 *
520 * @return bool
521 * @since 1.2.0
522 */
523 private function is_gateway_configured() {
524 if ( null === $this->gateway_configured ) {
525 // "Configured" means connected in any mode. Stripe's check is
526 // mode-agnostic; PayPal's is per-mode, so check both explicitly.
527 $this->gateway_configured = Stripe_Helper::is_stripe_connected()
528 || PayPal_Helper::is_paypal_connected( 'live' )
529 || PayPal_Helper::is_paypal_connected( 'test' );
530 }
531
532 return $this->gateway_configured;
533 }
534 }
535