| 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 |
|