PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / trunk
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management vtrunk
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 trunk, at inc/admin/notices.php

850 lines 27.7 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 * - Test mode notice : shown when a payment gateway is connected but
10 * the site is still in test mode, nudging the
11 * admin to switch to live mode.
12 * - Review notice (gateway) : shown when a payment gateway is connected in
13 * live mode but no live donation exists yet,
14 * after the 3-day install grace.
15 * - Setup gateway notice : shown instantly (no grace) when no payment
16 * gateway is connected at all.
17 *
18 * The four notices are mutually exclusive by construction. Priority is:
19 * live donation > gateway configured (test mode) > gateway configured (live
20 * mode) > no gateway.
21 *
22 * @package SureDonation
23 */
24
25 namespace SureDonation\Inc\Admin;
26
27 use SureDonation\Inc\Database\Tables\Donations;
28 use SureDonation\Inc\Helper;
29 use SureDonation\Inc\Payments\Payment_Helper;
30 use SureDonation\Inc\Payments\PayPal\PayPal_Helper;
31 use SureDonation\Inc\Payments\Stripe\Stripe_Helper;
32
33 // Exit if accessed directly.
34 if ( ! defined( 'ABSPATH' ) ) {
35 exit;
36 }
37
38 /**
39 * Admin Notices class.
40 *
41 * @since 1.2.0
42 */
43 class Notices {
44
45 /**
46 * Install-grace period, in seconds, before the review notices may appear.
47 *
48 * @var int
49 * @since 1.2.0
50 */
51 public const REVIEW_NOTICE_DELAY = 3 * DAY_IN_SECONDS;
52
53 /**
54 * WordPress.org review URL for the CTA.
55 *
56 * @var string
57 * @since 1.2.0
58 */
59 public const REVIEW_URL = 'https://wordpress.org/support/plugin/suredonation/reviews/#new-post';
60
61 /**
62 * Instance of this class.
63 *
64 * @var Notices|null
65 * @since 1.2.0
66 */
67 private static $instance = null;
68
69 /**
70 * Memoized "has at least one live donation" result.
71 *
72 * @var bool|null
73 * @since 1.2.0
74 */
75 private $has_live_donation = null;
76
77 /**
78 * Memoized "a payment gateway is configured" result.
79 *
80 * @var bool|null
81 * @since 1.2.0
82 */
83 private $gateway_configured = null;
84
85 /**
86 * Constructor.
87 *
88 * @since 1.2.0
89 */
90 private function __construct() {
91 // Ensure the notices library (and its priority-30 renderer) is loaded
92 // early, before the admin_notices hook fires.
93 if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
94 require_once SUREDONATION_DIR . 'inc/lib/astra-notices/class-bsf-admin-notices.php';
95 }
96
97 add_action( 'admin_notices', [ $this, 'display_review_notice_donation' ] );
98 add_action( 'admin_notices', [ $this, 'display_test_mode_notice' ] );
99 add_action( 'admin_notices', [ $this, 'display_review_notice_gateway' ] );
100 add_action( 'admin_notices', [ $this, 'display_setup_gateway_notice' ] );
101 add_action( 'admin_notices', [ $this, 'display_webhook_notice' ] );
102 add_action( 'admin_notices', [ $this, 'display_paypal_reconnect_notice' ] );
103
104 // Load the banner-notice styles from the admin <head> (not the late
105 // after-markup hook) so the banner never renders unstyled first.
106 add_action( 'admin_enqueue_scripts', [ $this, 'maybe_enqueue_banner_notice_style' ] );
107
108 add_action( 'wp_ajax_suredonation_notice_response', [ $this, 'handle_notice_response' ] );
109 }
110
111 /**
112 * Get instance of this class.
113 *
114 * @return Notices
115 * @since 1.2.0
116 */
117 public static function get_instance() {
118 if ( null === self::$instance ) {
119 self::$instance = new self();
120 }
121 return self::$instance;
122 }
123
124 /**
125 * Review notice shown after the first live donation (notice A).
126 *
127 * @return void
128 * @since 1.2.0
129 */
130 public function display_review_notice_donation() {
131 if ( ! Helper::current_user_can() ) {
132 return;
133 }
134
135 if ( ! apply_filters( 'suredonation_show_review_notice_donation', true ) ) {
136 return;
137 }
138
139 if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
140 return;
141 }
142
143 \BSF_Admin_Notices::add_notice(
144 [
145 'id' => 'sd-review-donation',
146 'type' => '',
147 'message' => $this->build_notice_markup(
148 esc_html__( 'You received your first donation with SureDonation!', 'suredonation' ),
149 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' ),
150 esc_url( self::REVIEW_URL ),
151 esc_html__( 'Rate SureDonation', 'suredonation' ),
152 esc_html__( 'Maybe later', 'suredonation' ),
153 esc_html__( 'I already did', 'suredonation' ),
154 WEEK_IN_SECONDS,
155 true
156 ),
157 'repeat-notice-after' => WEEK_IN_SECONDS,
158 // Test-mode takes priority: while the site is in test mode the
159 // "switch to live" warning is more useful than a review ask.
160 'show_if' => $this->is_three_days_elapsed() && $this->has_live_donation() && ! $this->should_show_test_mode_notice(),
161 'display-with-other-notices' => true,
162 ]
163 );
164
165 add_action( 'astra_notice_after_markup_sd-review-donation', [ $this, 'enqueue_notice_response_script' ] );
166 }
167
168 /**
169 * Test-mode notice shown when a gateway is connected but the site is still
170 * in test mode (notice A2).
171 *
172 * Nudges the admin to switch to live mode so real donations can be
173 * accepted. Takes priority over the gateway review notice: there is no
174 * point asking for a review while the site cannot yet take real money.
175 *
176 * @return void
177 * @since 1.3.0
178 */
179 public function display_test_mode_notice() {
180 if ( ! Helper::current_user_can() ) {
181 return;
182 }
183
184 if ( ! apply_filters( 'suredonation_show_test_mode_notice', true ) ) {
185 return;
186 }
187
188 if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
189 return;
190 }
191
192 \BSF_Admin_Notices::add_notice(
193 [
194 'id' => 'sd-test-mode',
195 'type' => '',
196 'message' => $this->build_test_mode_notice_markup(),
197 'repeat-notice-after' => WEEK_IN_SECONDS,
198 'show_if' => $this->should_show_test_mode_notice() && ! $this->should_show_webhook_notice(),
199 'display-with-other-notices' => true,
200 ]
201 );
202
203 add_action( 'astra_notice_after_markup_sd-test-mode', [ $this, 'enqueue_notice_response_script' ] );
204 }
205
206 /**
207 * Stripe webhook-not-configured notice.
208 *
209 * Shown on wp-admin pages when Stripe is connected but its webhook is not
210 * configured for the current mode, so donation/subscription statuses may not
211 * sync. Mirrors the SureForms webhook notice: a standard dismissible core
212 * notice (dismissal is per-page-load and reappears until the webhook is set).
213 * Takes priority over the test-mode banner, which is suppressed while this
214 * shows (matching the React dashboard notice chain).
215 *
216 * Hooked - admin_notices
217 *
218 * @return void
219 * @since 1.3.0
220 */
221 public function display_webhook_notice() {
222 if ( ! Helper::current_user_can() ) {
223 return;
224 }
225
226 if ( ! $this->should_show_webhook_notice() ) {
227 return;
228 }
229
230 // Load the analytics tracker so the configure-click and dismiss are
231 // recorded, matching the other notices (see handle_notice_response()).
232 $this->enqueue_notice_response_script();
233 ?>
234 <div id="sd-webhook-not-configured" class="notice notice-error is-dismissible">
235 <p>
236 <?php
237 printf(
238 /* translators: %1$s: link to configure the Stripe webhook */
239 esc_html__( 'Webhooks keep SureDonation in sync with Stripe by automatically updating donation and subscription data. Please %1$s the webhook.', 'suredonation' ),
240 sprintf(
241 '<a class="sd-notice-cta" href="%1$s">%2$s</a>',
242 esc_url( Payment_Helper::get_settings_url( 'stripe' ) ),
243 esc_html__( 'configure', 'suredonation' )
244 )
245 );
246 ?>
247 </p>
248 </div>
249 <?php
250 }
251
252 /**
253 * Review notice shown when a gateway is configured but there are no live
254 * donations yet (notice B).
255 *
256 * @return void
257 * @since 1.2.0
258 */
259 public function display_review_notice_gateway() {
260 if ( ! Helper::current_user_can() ) {
261 return;
262 }
263
264 if ( ! apply_filters( 'suredonation_show_review_notice_gateway', true ) ) {
265 return;
266 }
267
268 if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
269 return;
270 }
271
272 \BSF_Admin_Notices::add_notice(
273 [
274 'id' => 'sd-review-gateway',
275 'type' => '',
276 'message' => $this->build_notice_markup(
277 esc_html__( 'Your payment gateway is all set up!', 'suredonation' ),
278 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' ),
279 esc_url( self::REVIEW_URL ),
280 esc_html__( 'Rate SureDonation', 'suredonation' ),
281 esc_html__( 'Maybe later', 'suredonation' ),
282 esc_html__( 'I already did', 'suredonation' ),
283 WEEK_IN_SECONDS,
284 true
285 ),
286 'repeat-notice-after' => WEEK_IN_SECONDS,
287 // In test mode the test-mode notice takes this slot instead,
288 // keeping the notice chain mutually exclusive.
289 'show_if' => $this->is_three_days_elapsed() && ! $this->has_live_donation() && $this->is_gateway_configured() && ! $this->should_show_test_mode_notice(),
290 'display-with-other-notices' => true,
291 ]
292 );
293
294 add_action( 'astra_notice_after_markup_sd-review-gateway', [ $this, 'enqueue_notice_response_script' ] );
295 }
296
297 /**
298 * Setup notice shown instantly when no payment gateway is connected
299 * (notice C). No install-grace applies to this notice.
300 *
301 * @return void
302 * @since 1.2.0
303 */
304 public function display_setup_gateway_notice() {
305 if ( ! Helper::current_user_can() ) {
306 return;
307 }
308
309 if ( ! apply_filters( 'suredonation_show_setup_gateway_notice', true ) ) {
310 return;
311 }
312
313 if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
314 return;
315 }
316
317 \BSF_Admin_Notices::add_notice(
318 [
319 'id' => 'sd-setup-gateway',
320 'type' => '',
321 'message' => $this->build_setup_notice_markup(),
322 'repeat-notice-after' => WEEK_IN_SECONDS,
323 'show_if' => $this->should_show_setup_gateway_notice(),
324 'display-with-other-notices' => true,
325 ]
326 );
327
328 add_action( 'astra_notice_after_markup_sd-setup-gateway', [ $this, 'enqueue_notice_response_script' ] );
329 }
330
331 /**
332 * Enqueue the notice-response analytics script.
333 *
334 * Called via the astra_notice_after_markup_{id} hook so the script only
335 * loads when a SureDonation notice is actually rendered.
336 *
337 * @return void
338 * @since 1.2.0
339 */
340 public function enqueue_notice_response_script() {
341 if ( wp_script_is( 'suredonation-notice-response', 'enqueued' ) ) {
342 return;
343 }
344
345 wp_enqueue_script(
346 'suredonation-notice-response',
347 SUREDONATION_URL . 'assets/js/notice-response.js',
348 [],
349 SUREDONATION_VER,
350 true
351 );
352
353 wp_localize_script(
354 'suredonation-notice-response',
355 'suredonationNoticeResponse',
356 [
357 'ajaxurl' => admin_url( 'admin-ajax.php' ),
358 'nonce' => wp_create_nonce( 'suredonation_notice_response' ),
359 ]
360 );
361 }
362
363 /**
364 * Handle the notice-response AJAX request.
365 *
366 * Validates the request and records the analytics event for the notice
367 * button that was clicked.
368 *
369 * @return void
370 * @since 1.2.0
371 */
372 public function handle_notice_response() {
373 if ( ! check_ajax_referer( 'suredonation_notice_response', 'nonce', false ) ) {
374 wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'suredonation' ) ], 403 );
375 }
376
377 if ( ! Helper::current_user_can() ) {
378 wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'suredonation' ) ], 403 );
379 }
380
381 $notice_id = isset( $_POST['notice_id'] ) ? sanitize_text_field( wp_unslash( $_POST['notice_id'] ) ) : '';
382 $button = isset( $_POST['button'] ) ? sanitize_text_field( wp_unslash( $_POST['button'] ) ) : '';
383
384 $valid = [
385 'sd-review-donation' => [
386 'rate_suredonation' => 'review_notice_donation_cta',
387 'maybe_later' => 'review_notice_donation_snooze',
388 'dismissed' => 'review_notice_donation_dismiss',
389 ],
390 'sd-test-mode' => [
391 'switch_to_live' => 'test_mode_notice_cta',
392 'maybe_later' => 'test_mode_notice_snooze',
393 'dismissed' => 'test_mode_notice_dismiss',
394 ],
395 'sd-review-gateway' => [
396 'rate_suredonation' => 'review_notice_gateway_cta',
397 'maybe_later' => 'review_notice_gateway_snooze',
398 'dismissed' => 'review_notice_gateway_dismiss',
399 ],
400 'sd-setup-gateway' => [
401 'configure_gateway' => 'setup_gateway_notice_cta',
402 'maybe_later' => 'setup_gateway_notice_snooze',
403 'dismissed' => 'setup_gateway_notice_dismiss',
404 ],
405 'sd-paypal-reconnect' => [
406 'paypal_reconnect_notice_cta' => 'paypal_reconnect_notice_cta',
407 'paypal_reconnect_notice_dismiss' => 'paypal_reconnect_notice_dismiss',
408 ],
409 'sd-webhook-not-configured' => [
410 'configure_webhook' => 'webhook_notice_cta',
411 'dismissed' => 'webhook_notice_dismiss',
412 ],
413 ];
414
415 if ( ! isset( $valid[ $notice_id ][ $button ] ) ) {
416 wp_send_json_error( [ 'message' => __( 'Invalid parameters.', 'suredonation' ) ], 400 );
417 }
418
419 $event_name = $valid[ $notice_id ][ $button ];
420
421 $events = Analytics::events();
422 if ( null !== $events ) {
423 $events->track( $event_name, $button );
424 }
425
426 wp_send_json_success();
427 }
428
429 /**
430 * Build the shared HTML markup for admin notices.
431 *
432 * All text parameters must be pre-escaped by the caller (e.g. via
433 * esc_html__()). URL parameters must be pre-escaped via esc_url().
434 *
435 * @param string $heading The notice heading text (pre-escaped).
436 * @param string $message The notice body text (pre-escaped).
437 * @param string $cta_url The primary CTA URL (pre-escaped).
438 * @param string $cta_text The primary CTA button text (pre-escaped).
439 * @param string $snooze_text The snooze button text (pre-escaped).
440 * @param string $dismiss_text The dismiss button text (pre-escaped).
441 * @param int $snooze_duration Snooze duration in seconds for the data-repeat-notice-after attribute.
442 * @param bool $external_cta Whether the CTA opens in a new tab and also dismisses the notice
443 * via the astra-notice-close class. Default false.
444 * @return string The notice HTML markup.
445 * @since 1.2.0
446 */
447 private function build_notice_markup( $heading, $message, $cta_url, $cta_text, $snooze_text, $dismiss_text, $snooze_duration, $external_cta = false ) {
448 $image_path = esc_url( SUREDONATION_URL . 'images/suredonation-icon.svg' );
449 $cta_class = $external_cta ? 'astra-notice-close button-primary' : 'button-primary';
450 $cta_attrs = $external_cta ? ' target="_blank" rel="noopener noreferrer"' : '';
451
452 return sprintf(
453 '<div class="notice-image">
454 <img src="%1$s" class="custom-logo" alt="SureDonation" width="64" height="64" itemprop="logo">
455 </div>
456 <div class="notice-content">
457 <div class="notice-heading">
458 %2$s
459 </div>
460 %3$s<br />
461 <div class="astra-review-notice-container">
462 <a href="%4$s" class="%5$s"%6$s>
463 %7$s
464 </a>
465 <span class="dashicons dashicons-clock" aria-hidden="true"></span>
466 <a href="#" data-repeat-notice-after="%8$s" class="astra-notice-close">
467 %9$s
468 </a>
469 <span class="dashicons dashicons-smiley" aria-hidden="true"></span>
470 <a href="#" class="astra-notice-close">
471 %10$s
472 </a>
473 </div>
474 </div>',
475 $image_path,
476 $heading,
477 $message,
478 $cta_url,
479 esc_attr( $cta_class ),
480 $cta_attrs,
481 $cta_text,
482 $snooze_duration,
483 $snooze_text,
484 $dismiss_text
485 );
486 }
487
488 /**
489 * Build the markup for the "configure a payment gateway" setup notice
490 * (notice C).
491 *
492 * @return string The notice HTML markup.
493 * @since 1.2.0
494 */
495 private function build_setup_notice_markup() {
496 return $this->build_banner_notice_markup(
497 esc_html__( 'Your donation site is almost ready!', 'suredonation' ),
498 esc_html__( 'Connect Stripe or PayPal and you can start accepting donations today. It takes a few minutes.', 'suredonation' ),
499 Payment_Helper::get_settings_url( 'stripe' ),
500 esc_html__( 'Connect Stripe or PayPal', 'suredonation' ),
501 SUREDONATION_URL . 'images/payment-gateway-notice.png'
502 );
503 }
504
505 /**
506 * PayPal needs reconnecting in live mode notice.
507 *
508 * The partner client id used to be stored without a mode, so on a site that
509 * connected both test and live the last connect overwrote the other's value.
510 * A site left holding the sandbox id renders the sandbox SDK against a live
511 * merchant, PayPal refuses to reconcile that pairing, and no live donation
512 * can be approved at all — silently, since the donor never reaches approval
513 * and nothing is logged.
514 *
515 * The stored value is an opaque string with nothing distinguishing sandbox
516 * from production, so the environment cannot be recovered after the fact.
517 * Reconnecting in live mode writes the mode-specific value and settles it,
518 * which is all this notice asks for.
519 *
520 * Hooked - admin_notices
521 *
522 * @return void
523 * @since 1.5.1
524 */
525 public function display_paypal_reconnect_notice() {
526 if ( ! Helper::current_user_can() ) {
527 return;
528 }
529
530 if ( ! $this->should_show_paypal_reconnect_notice() ) {
531 return;
532 }
533
534 $this->enqueue_notice_response_script();
535 ?>
536 <div id="sd-paypal-reconnect" class="notice notice-error is-dismissible">
537 <p>
538 <?php
539 printf(
540 /* translators: %1$s: link to the PayPal payment settings */
541 esc_html__( 'PayPal needs reconnecting before it can take live donations on this site. Test and live are separate PayPal connections, and an earlier version stored one of them over the other. Please %1$s while in live mode. It takes a moment and no settings are lost.', 'suredonation' ),
542 sprintf(
543 '<a class="sd-notice-cta" href="%1$s">%2$s</a>',
544 esc_url( Payment_Helper::get_settings_url( 'paypal' ) ),
545 esc_html__( 'reconnect PayPal', 'suredonation' )
546 )
547 );
548 ?>
549 </p>
550 </div>
551 <?php
552 }
553
554 /**
555 * Whether the PayPal reconnect notice is eligible to show.
556 *
557 * Scoped as tightly as the stored data allows:
558 *
559 * - **Both modes connected.** The only state where the stored value is
560 * ambiguous. A live-only site's value must be the production id, because
561 * nothing else could have written it; a test-only site takes no live
562 * donations either way.
563 * - **The legacy key is still in use for live.** Keyed on the live value
564 * rather than on both being empty: reconnecting test first would otherwise
565 * clear the notice while live — the mode that handles real money — was
566 * still loading a possibly-sandbox id.
567 * - **No completed live PayPal donation.** One is proof the stored value is
568 * the production id, so there is nothing to fix. (A site whose only live
569 * PayPal donation was later refunded no longer counts here and would be
570 * asked to reconnect unnecessarily — one wasted reconnect, against
571 * silently losing every live donation.)
572 *
573 * A site that connected test and then live, so its last connect wrote the
574 * correct value, and which has not taken a live PayPal donation yet, is
575 * asked to reconnect when it does not need to. That case cannot be told
576 * apart from a broken one — which is exactly why the mode is not inferred.
577 *
578 * @return bool
579 * @since 1.5.1
580 */
581 private function should_show_paypal_reconnect_notice() {
582 if ( ! PayPal_Helper::is_paypal_connected( 'live' ) || ! PayPal_Helper::is_paypal_connected( 'test' ) ) {
583 return false;
584 }
585
586 $settings = PayPal_Helper::get_all_paypal_settings();
587
588 $legacy_client_id = isset( $settings['partner_client_id'] ) && is_string( $settings['partner_client_id'] )
589 ? $settings['partner_client_id']
590 : '';
591
592 if ( '' === $legacy_client_id ) {
593 return false;
594 }
595
596 $live_client_id = isset( $settings['partner_client_id_live'] ) && is_string( $settings['partner_client_id_live'] )
597 ? $settings['partner_client_id_live']
598 : '';
599
600 if ( '' !== $live_client_id ) {
601 return false;
602 }
603
604 // Cached like has_live_donation(): once a live PayPal donation exists the
605 // answer can never go back, so the count runs at most once per site.
606 if ( get_option( 'suredonation_has_live_paypal_donation' ) ) {
607 return false;
608 }
609
610 if ( Donations::count_live_completed( 'paypal' ) >= 1 ) {
611 update_option( 'suredonation_has_live_paypal_donation', 1, false );
612
613 return false;
614 }
615
616 return true;
617 }
618
619 /**
620 * Build the markup for the "switch to live mode" test-mode notice
621 * (notice A2).
622 *
623 * @return string The notice HTML markup.
624 * @since 1.3.0
625 */
626 private function build_test_mode_notice_markup() {
627 // The PayPal sandbox requirement is deliberately not here. It is gateway
628 // advice rather than something about the site being in test mode, and it
629 // belongs beside the mode control in payment settings, where a merchant
630 // is choosing the mode rather than being told about it.
631 $text = esc_html__( 'Supporters cannot donate while your site is in test mode. Anything they try now is a test and no money reaches you. Switch to live mode when you are ready to accept real donations.', 'suredonation' );
632
633 return $this->build_banner_notice_markup(
634 esc_html__( 'SureDonation is in test mode', 'suredonation' ),
635 $text,
636 Payment_Helper::get_settings_url(),
637 esc_html__( 'Switch to Live Mode', 'suredonation' )
638 );
639 }
640
641 /**
642 * Build the shared banner-notice markup (accent bar, icon, heading, body and
643 * a primary CTA, with an optional right-side illustration).
644 *
645 * Shared by the setup-gateway and test-mode notices; each is scoped by its
646 * wrapper id (#sd-setup-gateway / #sd-test-mode) in setup-gateway-notice.css
647 * so they can carry different accent colors from the same template.
648 *
649 * The text parameters must be pre-escaped by the caller (e.g. via
650 * esc_html__()); the URL and art path are escaped here.
651 *
652 * @param string $title The notice heading (pre-escaped).
653 * @param string $text The notice body text (pre-escaped).
654 * @param string $cta_url The primary CTA URL (raw; escaped here).
655 * @param string $cta_text The primary CTA button text (pre-escaped).
656 * @param string $art Optional right-side illustration URL (raw; escaped
657 * here). When empty, the banner drops the reserved
658 * art space via the --no-art modifier.
659 * @return string The notice HTML markup.
660 * @since 1.3.0
661 */
662 private function build_banner_notice_markup( $title, $text, $cta_url, $cta_text, $art = '' ) {
663 $has_art = '' !== $art;
664 $notice_class = $has_art ? 'sd-setup-notice' : 'sd-setup-notice sd-setup-notice--no-art';
665 $art_markup = $has_art
666 ? sprintf( '<img class="sd-setup-notice__art" src="%s" alt="" width="187" height="128" />', esc_url( $art ) )
667 : '';
668
669 return sprintf(
670 '<div class="%1$s">
671 <div class="sd-setup-notice__main">
672 <img class="sd-setup-notice__icon" src="%2$s" alt="" width="28" height="28" />
673 <div class="sd-setup-notice__body">
674 <h2 class="sd-setup-notice__title">%3$s</h2>
675 <p class="sd-setup-notice__text">%4$s</p>
676 <a href="%5$s" class="button button-primary sd-setup-notice__button">%6$s</a>
677 </div>
678 </div>
679 %7$s
680 </div>',
681 esc_attr( $notice_class ),
682 esc_url( SUREDONATION_URL . 'images/suredonation-icon.svg' ),
683 $title,
684 $text,
685 esc_url( $cta_url ),
686 $cta_text,
687 $art_markup
688 );
689 }
690
691 /**
692 * Enqueue the banner-notice stylesheet.
693 *
694 * @return void
695 * @since 1.2.0
696 */
697 public function enqueue_setup_notice_style() {
698 if ( wp_style_is( 'suredonation-setup-notice', 'enqueued' ) ) {
699 return;
700 }
701
702 wp_enqueue_style(
703 'suredonation-setup-notice',
704 SUREDONATION_URL . 'assets/css/setup-gateway-notice.css',
705 [],
706 SUREDONATION_VER
707 );
708 }
709
710 /**
711 * Enqueue the banner-notice stylesheet from the admin <head> when either
712 * banner notice (setup-gateway or test-mode) is eligible to show.
713 *
714 * Hooked on admin_enqueue_scripts (which runs before admin_head) and gated
715 * by the same conditions as the notices themselves, so the stylesheet is in
716 * the page head before the banner paints. This avoids the flash of unstyled
717 * content that occurred when the CSS was enqueued on the notice's
718 * after-markup hook (which fires at admin_notices priority 30, after styles
719 * have already been printed).
720 *
721 * @return void
722 * @since 1.2.0
723 */
724 public function maybe_enqueue_banner_notice_style() {
725 if ( ! Helper::current_user_can() ) {
726 return;
727 }
728
729 $setup_eligible = apply_filters( 'suredonation_show_setup_gateway_notice', true ) && $this->should_show_setup_gateway_notice();
730 $test_eligible = apply_filters( 'suredonation_show_test_mode_notice', true ) && $this->should_show_test_mode_notice();
731
732 if ( ! $setup_eligible && ! $test_eligible ) {
733 return;
734 }
735
736 $this->enqueue_setup_notice_style();
737 }
738
739 /**
740 * Whether the test-mode notice is eligible to show: a gateway is connected
741 * (in any mode) but the site is currently running in test mode. This fires
742 * regardless of past donations — a site switched back to test mode still
743 * needs the "switch to live" nudge — and takes priority over the review
744 * notices, which are suppressed while it is showing.
745 *
746 * @return bool
747 * @since 1.3.0
748 */
749 private function should_show_test_mode_notice() {
750 return $this->is_gateway_configured()
751 && 'test' === Payment_Helper::get_payment_mode();
752 }
753
754 /**
755 * Whether the webhook-not-configured notice is eligible to show: Stripe is
756 * connected but its webhook is not configured for the current mode.
757 *
758 * @return bool
759 * @since 1.3.0
760 */
761 private function should_show_webhook_notice() {
762 return Stripe_Helper::is_stripe_connected()
763 && ! Stripe_Helper::is_webhook_configured();
764 }
765
766 /**
767 * Whether the setup-gateway notice is eligible to show: no gateway is
768 * connected and no live donation has been recorded.
769 *
770 * @return bool
771 * @since 1.3.0
772 */
773 private function should_show_setup_gateway_notice() {
774 return ! $this->has_live_donation() && ! $this->is_gateway_configured();
775 }
776
777 /**
778 * Whether the 3-day install grace has elapsed.
779 *
780 * @return bool
781 * @since 1.2.0
782 */
783 private function is_three_days_elapsed() {
784 return ( time() - $this->get_install_time() ) >= self::REVIEW_NOTICE_DELAY;
785 }
786
787 /**
788 * Get (creating if missing) the plugin install timestamp.
789 *
790 * The activation hook seeds this on fresh installs; this getter back-fills
791 * it for sites that were already active before the option existed, so their
792 * grace period starts from the first admin pageload after the update.
793 *
794 * @return int Unix timestamp.
795 * @since 1.2.0
796 */
797 private function get_install_time() {
798 $install_time = Helper::get_integer_value( get_option( 'suredonation_install_time', 0 ) );
799
800 if ( ! $install_time ) {
801 $install_time = time();
802 update_option( 'suredonation_install_time', $install_time );
803 }
804
805 return $install_time;
806 }
807
808 /**
809 * Whether the site has at least one completed, live-mode donation.
810 *
811 * @return bool
812 * @since 1.2.0
813 */
814 private function has_live_donation() {
815 if ( null === $this->has_live_donation ) {
816 // Persist a monotonic flag: once the site has recorded a completed
817 // live donation it stays "true" for this notice's purpose, so we
818 // stop running COUNT(*) on every admin pageload once it is set.
819 if ( get_option( 'suredonation_has_live_donation' ) ) {
820 $this->has_live_donation = true;
821 } else {
822 $this->has_live_donation = Donations::count_live_completed() >= 1;
823
824 if ( $this->has_live_donation ) {
825 update_option( 'suredonation_has_live_donation', 1, false );
826 }
827 }
828 }
829
830 return $this->has_live_donation;
831 }
832
833 /**
834 * Whether a payment gateway (Stripe or PayPal) is connected, in any mode.
835 *
836 * @return bool
837 * @since 1.2.0
838 */
839 private function is_gateway_configured() {
840 if ( null === $this->gateway_configured ) {
841 // "Configured" means connected in any mode; delegated to the shared
842 // Payment_Helper check (memoized here so repeated notice-chain reads
843 // only resolve it once per request).
844 $this->gateway_configured = Payment_Helper::is_any_gateway_connected();
845 }
846
847 return $this->gateway_configured;
848 }
849 }
850