PluginProbe
Post Grid Gutenberg Blocks – PostX / 5.1.1
Post Grid Gutenberg Blocks – PostX v5.1.1
5.1.1 5.1.0 5.0.41 5.0.39 5.0.38 5.0.37 5.0.36 5.0.35 5.0.34 5.0.33 5.0.32 5.0.31 5.0.30 5.0.29 5.0.28 5.0.27 5.0.26 5.0.25 5.0.23 5.0.24 5.0.22 5.0.21 5.0.20 5.0.19 5.0.18 All 240 releases
← All changes | includes/notice/class-notice.php +206 -840 5.0.29 → 5.1.1 View file →
@@ -3,10 +3,18 @@
3 3
4 4 defined( 'ABSPATH' ) || exit;
5 5
6 6 use ULTP\Includes\Durbin\Xpo;
7 -use ULTP\Includes\Durbin\DurbinClient;
8 7
8 +require_once __DIR__ . '/interface-notice-type.php';
9 +require_once __DIR__ . '/class-notice-assets.php';
10 +require_once __DIR__ . '/class-abstract-campaign-notice.php';
11 +require_once __DIR__ . '/types/class-banner-notice.php';
12 +require_once __DIR__ . '/types/class-content-notice.php';
13 +require_once __DIR__ . '/types/class-image-notice.php';
14 +require_once __DIR__ . '/types/class-consent-notice.php';
15 +require_once __DIR__ . '/class-promo-links.php';
16 +
9 17 /**
10 18 * Plugin Notice
11 19 */
12 20 class Notice {
@@ -12,20 +20,20 @@
12 20 class Notice {
13 21
14 22
15 23 /**
16 - * Notice version
24 + * Campaigns showing on this request. Null until first worked out.
17 25 *
18 - * @var string
26 + * @var Abstract_Campaign_Notice[]|null
19 27 */
20 - private $notice_version = 'v5132';
28 + private $active_campaigns = null;
21 29
22 30 /**
23 - * Notice JS/CSS applied
31 + * Data-collection consent notice. Null until first needed.
24 32 *
25 - * @var boolean
33 + * @var Consent_Notice|null
26 34 */
27 - private $notice_js_css_applied = false;
35 + private $consent_notice = null;
28 36
29 37 /**
30 38 * Notice Priority
31 39 *
@@ -45,15 +53,17 @@
45 53 */
46 54 public function __construct() {
47 55 add_action( 'admin_notices', array( $this, 'admin_notices_callback' ) );
48 56 add_action( 'admin_init', array( $this, 'set_dismiss_notice_callback' ) );
57 + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
49 58
50 59 // REST API routes.
51 60 add_action( 'rest_api_init', array( $this, 'register_rest_route' ) );
52 61
53 - // Woocommerce Install Action
54 - // add_action( 'wp_ajax_ultp_install', array( $this, 'install_activate_plugin' ) ); // this ajax not called anywhere in future need to removed, that is arise patchstack security issue
55 62 add_filter( 'xpo_active_notice_lists', array( $this, 'handle_xpo_active_notice_lists' ), 99, 1 );
63 +
64 + // Upgrade link on the Plugins page and upgrade item in the PostX menu.
65 + new Promo_Links();
56 66 }
57 67
58 68 /**
59 69 * Registers REST API endpoints.
@@ -69,8 +79,20 @@
69 79 'callback' => array( $this, 'hello_bar_callback' ),
70 80 'permission_callback' => function () {
71 81 return current_user_can( 'manage_options' );
72 82 },
83 + 'args' => array(
84 + 'type' => array(
85 + 'type' => 'string',
86 + 'required' => true,
87 + 'enum' => array( 'hello_bar' ),
88 + ),
89 + 'id' => array(
90 + 'type' => 'string',
91 + 'required' => true,
92 + 'enum' => self::get_hellobar_keys(),
93 + ),
94 + ),
73 95 ),
74 96 );
75 97
76 98 foreach ( $routes as $route ) {
@@ -81,8 +103,9 @@
81 103 array(
82 104 'methods' => $route['methods'],
83 105 'callback' => $route['callback'],
84 106 'permission_callback' => $route['permission_callback'],
107 + 'args' => isset( $route['args'] ) ? $route['args'] : array(),
85 108 ),
86 109 )
87 110 );
88 111 }
@@ -93,21 +116,34 @@
93 116 *
94 117 * @return array
95 118 */
96 119 public static function get_hellobar_config() {
97 - return array(
98 - // Flash sale.
99 - 'ultp_helloBar_flash_sale_2026_4' => Xpo::get_transient_without_cache( 'ultp_helloBar_flash_sale_2026_4' ),
100 - // Surprise sale.
101 - 'ultp_helloBar_surprise_sale_2026_5' => Xpo::get_transient_without_cache( 'ultp_helloBar_surprise_sale_2026_5' ),
102 - // Massive sale.
103 - 'ultp_helloBar_massive_sale_2026_6' => Xpo::get_transient_without_cache( 'ultp_helloBar_massive_sale_2026_6' ),
104 - // Final hours sale.
105 - 'ultp_helloBar_final_hours_sale_2026_7' => Xpo::get_transient_without_cache( 'ultp_helloBar_final_hours_sale_2026_7' ),
106 - );
120 + $config = array();
121 + foreach ( self::get_hellobar_keys() as $key ) {
122 + $config[ $key ] = Xpo::get_transient_without_cache( $key );
123 + }
124 + return $config;
107 125 }
108 126
109 127 /**
128 + * Hello bar ids the dashboard may dismiss, from Campaigns::hellobar_keys().
129 + *
130 + * class-campaigns.php is edited for every sale, so it is loaded inside a try: a mistake in it
131 + * empties this list (hello bars can't be closed until it's fixed) instead of breaking wp-admin.
132 + *
133 + * @return string[]
134 + */
135 + private static function get_hellobar_keys() {
136 + try {
137 + require_once __DIR__ . '/class-campaigns.php';
138 + $keys = Campaigns::hellobar_keys();
139 + } catch ( \Throwable $e ) {
140 + return array();
141 + }
142 + return is_array( $keys ) ? array_values( array_filter( $keys, 'is_string' ) ) : array();
143 + }
144 +
145 + /**
110 146 * Handles Hello Bar dismissal action via REST API .
111 147 *
112 148 * @param \WP_REST_Request $request REST request object .
113 149 * @return \WP_REST_Response
@@ -112,13 +148,13 @@
112 148 * @param \WP_REST_Request $request REST request object .
113 149 * @return \WP_REST_Response
114 150 */
115 151 public function hello_bar_callback( \WP_REST_Request $request ) {
116 - $request_params = $request->get_params();
117 - $type = isset( $request_params['type'] ) ? $request_params['type'] : '';
118 - $id = isset( $request_params['id'] ) ? $request_params['id'] : '';
152 + $type = $request->get_param( 'type' );
153 + $id = $request->get_param( 'id' );
119 154
120 - if ( 'hello_bar' === $type && ! empty( $id ) ) {
155 + // The route's args already reject unknown ids; check again in case this is called directly.
156 + if ( 'hello_bar' === $type && is_string( $id ) && in_array( $id, self::get_hellobar_keys(), true ) ) {
121 157 Xpo::set_transient_without_cache( $id, 'hide', 1296000 );
122 158 }
123 159
124 160 return new \WP_REST_Response(
@@ -140,19 +176,11 @@
140 176 if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['wpnonce'] ?? '' ) ), 'ultp-nonce' ) ) {
141 177 return;
142 178 }
143 179
144 - $durbin_key = sanitize_text_field( wp_unslash( $_GET['ultp_durbin_key'] ?? '' ) );
145 -
146 180 // Durbin notice dismiss.
147 - if ( ! empty( $durbin_key ) ) {
148 - Xpo::set_transient_without_cache( 'ultp_durbin_notice_' . $durbin_key, 'off' );
181 + Consent_Notice::handle_dismiss_request();
149 182
150 - if ( 'get' === sanitize_text_field( wp_unslash( $_GET['ultp_get_durbin'] ?? '' ) ) ) {
151 - DurbinClient::send( DurbinClient::ACTIVATE_ACTION );
152 - }
153 - }
154 -
155 183 // Install notice dismiss.
156 184 $install_key = sanitize_text_field( wp_unslash( $_GET['ultp_install_key'] ?? '' ) );
157 185 if ( ! empty( $install_key ) ) {
158 186 Xpo::set_transient_without_cache( 'ultp_install_notice_' . $install_key, 'off' );
@@ -186,8 +214,10 @@
186 214 public function ultp_dashboard_notice_callback() {
187 215 if ( $this->is_available_for_notice() ) {
188 216 $this->ultp_dashboard_banner_notice();
189 217 $this->ultp_dashboard_content_notice();
218 + $this->ultp_dashboard_image_banner_notice();
219 +
190 220 }
191 221 }
192 222
193 223 /**
@@ -192,372 +222,45 @@
192 222
193 223 /**
194 224 * Dashboard Banner Notice
195 225 *
196 - * @return void
226 + * @param bool $return_bool True to only report whether a banner would show.
227 + * @return bool|void
197 228 */
198 229 public function ultp_dashboard_banner_notice( $return_bool = false ) {
199 - $ultp_db_nonce = wp_create_nonce( 'ultp-nonce' );
200 - $banner_notices = array(
201 - // FLash sale.
202 - array(
203 - 'key' => 'ultp_banner_flash_sale_2026_1',
204 - 'start' => '2026-05-18 00:00 Asia/Dhaka',
205 - 'end' => '2026-05-21 23:59 Asia/Dhaka', // format YY-MM-DD always set time 23:59 and zone Asia/Dhaka.
230 + return $this->render_campaigns( Banner_Notice::class, $return_bool );
231 + }
206 232
207 - 'brand_color' => '#0322ff',
208 -
209 - 'left_image' => ULTP_URL . 'assets/img/dashboard_banner/flash_sale/offer.png',
210 - 'right_image' => ULTP_URL . 'assets/img/dashboard_banner/flash_sale/btn.png',
211 - 'bg_image' => ULTP_URL . 'assets/img/dashboard_banner/flash_sale/bg.png',
212 - 'text' => 'Deal ending soon',
213 - 'countdown_duration' => 259200, // Duration in seconds.
214 - // 'countdown_color' => '#0322ff',
215 - 'countdown_color' => '#3CF357',
216 - 'url' => Xpo::generate_utm_link(
217 - array(
218 - 'utmKey' => 'flash_sale_content',
219 - )
220 - ),
221 -
222 - 'visibility' => ! Xpo::is_lc_active(),
223 - ),
224 - // surprise sale
225 - array(
226 - 'key' => 'ultp_banner_surprise_sale_2026_1',
227 - 'start' => '2026-05-29 00:00 Asia/Dhaka',
228 - 'end' => '2026-06-01 23:59 Asia/Dhaka', // format YY-MM-DD always set time 23:59 and zone Asia/Dhaka.
229 -
230 - 'brand_color' => '#0322ff',
231 -
232 - 'left_image' => ULTP_URL . 'assets/img/dashboard_banner/surprise_sale/offer.png',
233 - 'right_image' => ULTP_URL . 'assets/img/dashboard_banner/surprise_sale/btn.png',
234 - 'bg_image' => ULTP_URL . 'assets/img/dashboard_banner/surprise_sale/bg.png',
235 - 'text' => 'Limited Time Offer',
236 - 'countdown_duration' => 259200, // Duration in seconds.
237 - // 'countdown_color' => '#0322ff',
238 - 'countdown_color' => '#3CF357',
239 - 'url' => Xpo::generate_utm_link(
240 - array(
241 - 'utmKey' => 'surprise_sale_content',
242 - )
243 - ),
244 -
245 - 'visibility' => ! Xpo::is_lc_active(),
246 - ),
247 - // massive sale
248 - array(
249 - 'key' => 'ultp_banner_massive_sale_2026_1',
250 - 'start' => '2026-06-17 00:00 Asia/Dhaka',
251 - 'end' => '2026-06-20 23:59 Asia/Dhaka', // format YY-MM-DD always set time 23:59 and zone Asia/Dhaka.
252 -
253 - 'brand_color' => '#0322ff',
254 -
255 - 'left_image' => ULTP_URL . 'assets/img/dashboard_banner/massive_sale/offer.png',
256 - 'right_image' => ULTP_URL . 'assets/img/dashboard_banner/massive_sale/btn.png',
257 - 'bg_image' => ULTP_URL . 'assets/img/dashboard_banner/massive_sale/bg.png',
258 - 'text' => 'Deal ending soon',
259 - 'countdown_duration' => 259200, // Duration in seconds.
260 - // 'countdown_color' => '#0322ff',
261 - 'countdown_color' => '#3CF357',
262 - 'url' => Xpo::generate_utm_link(
263 - array(
264 - 'utmKey' => 'massive_sale_content',
265 - )
266 - ),
267 -
268 - 'visibility' => ! Xpo::is_lc_active(),
269 - ),
270 - // Final hours sale
271 - array(
272 - 'key' => 'ultp_banner_final_hours_sale_2026_1',
273 - 'start' => '2026-06-28 00:00 Asia/Dhaka',
274 - 'end' => '2026-06-30 23:59 Asia/Dhaka', // format YY-MM-DD always set time 23:59 and zone Asia/Dhaka.
275 -
276 - 'brand_color' => '#0322ff',
277 -
278 - 'left_image' => ULTP_URL . 'assets/img/dashboard_banner/final_hours_sale/offer.png',
279 - 'right_image' => ULTP_URL . 'assets/img/dashboard_banner/final_hours_sale/btn.png',
280 - 'bg_image' => ULTP_URL . 'assets/img/dashboard_banner/final_hours_sale/bg.png',
281 - 'text' => 'Hurry Before It Ends!',
282 - 'countdown_duration' => 259200, // Duration in seconds.
283 - // 'countdown_color' => '#0322ff',
284 - 'countdown_color' => '#3CF357',
285 - 'url' => Xpo::generate_utm_link(
286 - array(
287 - 'utmKey' => 'final_hours_content',
288 - )
289 - ),
290 - 'visibility' => ! Xpo::is_lc_active(),
291 - ),
292 - );
293 - foreach ( $banner_notices as $notice ) {
294 - $notice_key = isset( $notice['key'] ) ? $notice['key'] : $this->notice_version;
295 -
296 - if ( isset( $_GET['ultp_notice'] ) && $notice_key === sanitize_text_field(wp_unslash($_GET['ultp_notice'])) ) { // phpcs:ignore
297 - continue;
233 + /**
234 + * Queue the stylesheets and scripts for the notices that will show on this page.
235 + *
236 + * Runs on admin_enqueue_scripts, before admin_notices, so the styles load in <head>.
237 + *
238 + * @return void
239 + */
240 + public function enqueue_assets() {
241 + if ( $this->is_available_for_notice() ) {
242 + foreach ( $this->get_active_campaigns() as $campaign ) {
243 + $campaign->enqueue_assets();
298 244 }
299 -
300 - $current_time = gmdate( 'U' );
301 - $notice_start = gmdate( 'U', strtotime( $notice['start'] ) );
302 - $notice_end = gmdate( 'U', strtotime( $notice['end'] ) );
303 - if ( $current_time >= $notice_start && $current_time <= $notice_end && $notice['visibility'] ) {
304 -
305 - $notice_transient = Xpo::get_transient_without_cache( 'ultp_get_pro_notice_' . $notice_key );
306 -
307 - if ( 'off' === $notice_transient ) {
308 - continue;
309 - }
310 - if ( $return_bool ) { // Early return for Other plugin notice.
311 - return true;
312 - }
313 - if ( ! $this->notice_js_css_applied ) {
314 - $this->ultp_banner_notice_js();
315 - $this->notice_js_css_applied = true;
316 - }
317 - $query_args = array(
318 - 'ultp_notice' => $notice_key,
319 - 'wpnonce' => $ultp_db_nonce,
320 - );
321 - if ( isset( $notice['repeat_interval'] ) && $notice['repeat_interval'] ) {
322 - $query_args['ultp_interval'] = $notice['repeat_interval'];
323 - }
324 - ?>
325 - <style type="text/css">
326 - .ultp-notice-wrapper.ultp-banner-notice {
327 - height: auto !important;
328 - padding: 0 !important;
329 - position: relative;
330 - box-sizing: border-box;
331 - background-repeat: no-repeat;
332 - background-size: cover;
333 - background-position: center;
334 - }
335 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-link {
336 - width: 100%;
337 - text-decoration: none;
338 - display: block;
339 - }
340 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-content {
341 - display: flex;
342 - justify-content: space-between;
343 - align-items: center;
344 - max-width: 700px;
345 - margin: 0 auto;
346 - padding: 10px 16px;
347 - gap: 16px;
348 - }
349 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-side-image {
350 - display: block;
351 - max-width: 100%;
352 - height: auto;
353 - max-height: 32px;
354 - }
355 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-main .ultp-banner-main-text {
356 - color: #ffffff;
357 - }
358 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-main {
359 - display: flex;
360 - flex-direction: column;
361 - gap: 4px;
362 - align-items: center;
363 - justify-content: center;
364 - font-weight: 700;
365 - font-size: 18px;
366 - color: #333333;
367 - line-height: 1.2;
368 - text-align: center;
369 - }
370 -
371 - @media screen and (max-width: 1100px) {
372 - /* .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-content {
373 - flex-direction: column;
374 - } */
375 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-content .ultp-banner-main-text {
376 - display: none;
377 - }
378 - }
379 - @media screen and (max-width: 490px) {
380 - .ultp-notice-wrapper.ultp-banner-notice {
381 - display: none;
382 - }
383 - }
384 - @media screen and (max-width: 782px) {
385 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-content {
386 - justify-content: center;
387 - padding: 12px 32px 12px 12px;
388 - }
389 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-main {
390 - font-size: 22px;
391 - line-height: 28px;
392 - }
393 - }
394 - @media screen and (max-width: 480px) {
395 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-content {
396 - padding: 10px 32px 10px 10px;
397 - }
398 - .ultp-notice-wrapper.ultp-banner-notice .ultp-banner-main {
399 - font-size: 18px;
400 - line-height: 24px;
401 - }
402 - }
403 - </style>
404 - <div
405 - class="ultp-notice-wrapper ultp-banner-notice notice"
406 - style="
407 - border-left: 3px solid <?php echo esc_attr( $notice['brand_color'] ); ?>;
408 - background-image: url('<?php echo esc_attr( $notice['bg_image'] ); ?>');
409 - ">
410 - <a
411 - class="wc-dismiss-notice dashicons dashicons-no-alt"
412 - style="
413 - position: absolute;
414 - top: 1px;
415 - right: 1px;
416 - border-radius: 50%;
417 - background-color: black;
418 - color: white;
419 - font-size: 14px;
420 - display: flex;
421 - align-items: center;
422 - justify-content: center;
423 - "
424 - aria-label="<?php esc_html_e( 'Close Banner', 'product-addons' ); ?>"
425 - href="<?php echo esc_url( add_query_arg( $query_args ) ); ?>">
426 - </a>
427 -
428 - <a class="ultp-banner-link" target="_blank" href="<?php echo esc_url( $notice['url'] ); ?>">
429 - <div class="ultp-banner-content">
430 - <img class="ultp-banner-side-image" loading="lazy" src="<?php echo esc_url( $notice['left_image'] ); ?>" />
431 - <div class="ultp-banner-main">
432 - <span class="ultp-banner-main-text">
433 - <?php echo esc_html( $notice['text'] ); ?>
434 - </span>
435 - <div
436 - class="ultp-notice-countdown"
437 - style="
438 - color: <?php echo esc_attr( $notice['countdown_color'] ); ?>;
439 - "
440 - data-notice-key="<?php echo esc_attr( $notice_key . '-countdown' ); ?>"
441 - data-duration="<?php echo esc_attr( $notice['countdown_duration'] ); ?>">
442 - 00:00:00:00
443 - </div>
444 - </div>
445 - <img class="ultp-banner-side-image" loading="lazy" src="<?php echo esc_url( $notice['right_image'] ); ?>" />
446 - </div>
447 - </a>
448 - </div>
449 - <?php
450 - }
451 245 }
246 + $consent = $this->get_consent_notice();
247 + if ( $consent->should_display() ) {
248 + $consent->enqueue_assets();
249 + }
452 250 }
453 251
454 252 /**
455 253 * Banner JS
456 254 *
255 + * @deprecated The countdown script is now the file assets/js/ultp-notice-countdown.js. This queues it.
256 + *
457 257 * @return void
458 258 */
459 259 public function ultp_banner_notice_js() {
460 - ?>
461 - <script type="text/javascript">
462 - jQuery(function($) {
463 - 'use strict';
464 -
465 - const storagePrefix = 'ultp_notice_countdown_';
466 -
467 - const formatCountdown = function(seconds) {
468 - const days = Math.floor(seconds / 86400);
469 - const hours = Math.floor((seconds % 86400) / 3600);
470 - const minutes = Math.floor((seconds % 3600) / 60);
471 - const secs = seconds % 60;
472 -
473 - return String(days).padStart(2, '0') + ':' + String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0') + ':' + String(secs).padStart(2, '0');
474 - };
475 -
476 - const parseDurationToSeconds = function(duration) {
477 - if (typeof duration === 'number' && Number.isFinite(duration) && duration > 0) {
478 - return Math.floor(duration);
479 - }
480 -
481 - const durationString = String(duration || '').trim();
482 - if (/^\d+$/.test(durationString)) {
483 - return parseInt(durationString, 10);
484 - }
485 -
486 - return 0;
487 - };
488 -
489 - const nowInSeconds = function() {
490 - return Math.floor(Date.now() / 1000);
491 - };
492 -
493 - $('.ultp-notice-countdown').each(function() {
494 - const countdownElement = $(this);
495 - const noticeKey = String(countdownElement.data('noticeKey') || '');
496 - const duration = parseDurationToSeconds(countdownElement.data('duration'));
497 -
498 - if (!noticeKey || duration <= 0) {
499 - return;
500 - }
501 -
502 - const storageKey = storagePrefix + noticeKey;
503 - let endAt = 0;
504 -
505 - try {
506 - const storedDataRaw = window.localStorage.getItem(storageKey);
507 - if (storedDataRaw) {
508 - const storedData = JSON.parse(storedDataRaw);
509 - if (storedData && parseInt(storedData.duration, 10) === duration) {
510 - endAt = parseInt(storedData.endAt, 10) || 0;
511 - }
512 - }
513 - } catch (error) {
514 - endAt = 0;
515 - }
516 -
517 - const saveTimerState = function(nextEndAt) {
518 - try {
519 - window.localStorage.setItem(
520 - storageKey,
521 - JSON.stringify({
522 - endAt: nextEndAt,
523 - duration: duration,
524 - })
525 - );
526 - } catch (error) {
527 - // No-op.
528 - }
529 - };
530 -
531 - const resetTimer = function(currentTime) {
532 - endAt = currentTime + duration;
533 - saveTimerState(endAt);
534 - };
535 -
536 - const tick = function() {
537 - const currentTime = nowInSeconds();
538 -
539 - if (endAt <= currentTime) {
540 - resetTimer(currentTime);
541 - }
542 -
543 - const remaining = Math.max(endAt - currentTime, 0);
544 - countdownElement.text(formatCountdown(remaining));
545 - };
546 -
547 - if (endAt <= nowInSeconds()) {
548 - resetTimer(nowInSeconds());
549 - }
550 -
551 - tick();
552 - window.setInterval(tick, 1000);
553 - });
554 - });
555 - </script>
556 - <?php
260 + Notice_Assets::enqueue_countdown_script();
557 261 }
558 262
559 -
560 263 /**
561 264 * The Durbin Html
562 265 *
563 266 * @return void
@@ -562,475 +265,33 @@
562 265 *
563 266 * @return void
564 267 */
565 268 public function ultp_dashboard_durbin_notice_callback() {
566 - $durbin_key = 'ultp_durbin_dc1';
567 -
568 - if (
569 - isset( $_GET['ultp_durbin_key'] ) || // phpcs:ignore
570 - 'off' === Xpo::get_transient_without_cache( 'ultp_durbin_notice_' . $durbin_key )
571 - ) {
572 - return;
269 + $consent = $this->get_consent_notice();
270 + if ( $consent->should_display() ) {
271 + $this->render_safely( $consent );
573 272 }
273 + }
574 274
575 - if ( ! $this->notice_js_css_applied ) {
576 - $this->notice_js_css_applied = true;
275 + /**
276 + * The data-collection consent notice, created once per request.
277 + *
278 + * @return Consent_Notice
279 + */
280 + private function get_consent_notice() {
281 + if ( null === $this->consent_notice ) {
282 + $this->consent_notice = new Consent_Notice();
577 283 }
578 -
579 - $ultp_db_nonce = wp_create_nonce( 'ultp-nonce' );
580 -
581 - ?>
582 - <style>
583 - .ultp-consent-box {
584 - width: 656px;
585 - padding: 16px !important;
586 - border: 1px solid #070707;
587 - border-left-width: 4px;
588 - border-radius: 4px;
589 - background-color: #fff;
590 - position: relative;
591 - width: 100%;
592 - box-sizing: border-box;
593 - }
594 - .ultp-consent-content {
595 - display: flex;
596 - justify-content: flex-start;
597 - align-items: flex-end;
598 - gap: 26px;
599 - }
600 -
601 - .ultp-consent-text-first {
602 - font-size: 14px;
603 - font-weight: 600;
604 - color: #070707;
605 - }
606 - .ultp-consent-text-last {
607 - margin: 4px 0 0;
608 - font-size: 14px;
609 - color: #070707;
610 - }
611 -
612 - .ultp-consent-accept {
613 - background-color: #070707;
614 - color: #fff !important;
615 - border: none;
616 - padding: 6px 10px;
617 - border-radius: 4px;
618 - cursor: pointer;
619 - font-size: 12px;
620 - font-weight: 600;
621 - text-decoration: none;
622 - }
623 - .ultp-consent-accept:hover {
624 - background-color:rgb(38, 38, 38);
625 - color: #fff;
626 - }
627 - </style>
628 - <div class="ultp-consent-box ultp-notice-wrapper notice data_collection_notice">
629 - <div class="ultp-consent-content">
630 - <div class="ultp-consent-text">
631 - <div class="ultp-consent-text-first"><?php esc_html_e( 'Want to help make PostX even more awesome?', 'ultimate-post' ); ?></div>
632 - <div class="ultp-consent-text-last">
633 - <?php esc_html_e( 'Allow us to collect diagnostic data and usage information. see ', 'ultimate-post' ); ?>
634 - <a href="https://www.wpxpo.com/data-collection-policy/" target="_blank" ><?php esc_html_e( 'what we collect.', 'ultimate-post' ); ?></a>
635 - </div>
636 - </div>
637 - <a
638 - class="ultp-consent-accept"
639 - href=
640 - <?php
641 - echo esc_url(
642 - add_query_arg(
643 - array(
644 - 'ultp_durbin_key' => $durbin_key,
645 - 'ultp_get_durbin' => 'get',
646 - 'wpnonce' => $ultp_db_nonce,
647 - )
648 - )
649 - );
650 - ?>
651 - class="ultp-notice-close"
652 - ><?php esc_html_e( 'Accept & Close', 'ultimate-post' ); ?></a>
653 - </div>
654 - <a href=
655 - <?php
656 - echo esc_url(
657 - add_query_arg(
658 - array(
659 - 'ultp_durbin_key' => $durbin_key,
660 - 'wpnonce' => $ultp_db_nonce,
661 - )
662 - )
663 - );
664 - ?>
665 - class="ultp-notice-close"
666 - style="
667 - position: absolute;
668 - right: 2px;
669 - top: 5px;
670 - text-decoration: unset;
671 - color: #b6b6b6;
672 - font-family: dashicons;
673 - font-size: 16px;
674 - font-style: normal;
675 - font-weight: 400;
676 - line-height: 20px;
677 - "
678 - >
679 - <span
680 - style="font-size: 14px;"
681 - class="ultp-notice-close-icon dashicons dashicons-dismiss"> </span></a>
682 - </div>
683 - <?php
284 + return $this->consent_notice;
684 285 }
685 286 /**
686 287 * Dashboard Content Notice
687 288 *
688 - * @return void
289 + * @param bool $return_bool True to only report whether a content notice would show.
290 + * @return bool|void
689 291 */
690 292 public function ultp_dashboard_content_notice( $return_bool = false ) {
691 - $content_notices = array(
692 - // Flash sale
693 - array(
694 - 'key' => 'ultp_dashboard_content_notice_flash_sale_brand_logos',
695 - 'start' => '2026-05-07 00:00 Asia/Dhaka',
696 - 'end' => '2026-05-12 23:59 Asia/Dhaka',
697 - 'url' => Xpo::generate_utm_link(
698 - array(
699 - 'utmKey' => 'flash_sale',
700 - )
701 - ),
702 - 'visibility' => ! Xpo::is_lc_active(),
703 - 'content_heading' => __( 'Flash Sale:', 'ultimate-post' ),
704 - 'content_subheading' => __( 'Enjoy %s on PostX Pro.', 'ultimate-post' ),
705 - 'discount_content' => ' up to 45% Off',
706 - 'brand_color' => '#0322ff',
707 - 'icon' => ULTP_URL . 'assets/img/dashboard_banner/brand_logo_round.svg',
708 - 'button_text' => __( 'Claim Your Discount!', 'ultimate-post' ),
709 - 'is_discount_logo' => true,
710 - 'border_color' => '#0322ff',
711 - ),
712 - array(
713 - 'key' => 'ultp_dashboard_content_notice_flash_sale_discount_logo',
714 - 'start' => '2026-05-13 00:00 Asia/Dhaka',
715 - 'end' => '2026-05-17 23:59 Asia/Dhaka',
716 - 'url' => Xpo::generate_utm_link(
717 - array(
718 - 'utmKey' => 'flash_sale',
719 - )
720 - ),
721 - 'visibility' => ! Xpo::is_lc_active(),
722 - 'content_heading' => __( 'Flash Sale:', 'ultimate-post' ),
723 - 'content_subheading' => __( 'Enjoy %s on PostX Pro.', 'ultimate-post' ),
724 - 'discount_content' => ' up to 45% Off',
725 - 'brand_color' => '#0322ff',
726 - 'icon' => ULTP_URL . 'assets/img/dashboard_banner/flash_sale_45_discount_logo.png',
727 - 'button_text' => __( 'Upgrade Now!', 'ultimate-post' ),
728 - 'is_discount_logo' => true,
729 - 'border_color' => '#0322ff',
730 - ),
731 -
732 - // Surprise Sale
733 - array(
734 - 'key' => 'ultp_dashboard_content_notice_surprise_sale_brand_logo',
735 - 'start' => '2026-05-22 00:00 Asia/Dhaka',
736 - 'end' => '2026-05-25 23:59 Asia/Dhaka',
737 - 'url' => Xpo::generate_utm_link(
738 - array(
739 - 'utmKey' => 'surprise_sale',
740 - )
741 - ),
742 - 'visibility' => ! Xpo::is_lc_active(),
743 - 'content_heading' => __( 'Surprise Sale:', 'ultimate-post' ),
744 - 'content_subheading' => __( 'Enjoy %s on PostX Pro.', 'ultimate-post' ),
745 - 'discount_content' => ' up to 50% Off',
746 - 'brand_color' => '#0322ff',
747 - 'icon' => ULTP_URL . 'assets/img/dashboard_banner/brand_logo_round.svg',
748 - 'button_text' => __( 'Claim Your Discount!', 'ultimate-post' ),
749 - 'is_discount_logo' => true,
750 - 'border_color' => '#0322ff',
751 - ),
752 - array(
753 - 'key' => 'ultp_dashboard_content_notice_surprise_sale_discount_logo',
754 - 'start' => '2026-05-26 00:00 Asia/Dhaka',
755 - 'end' => '2026-05-28 23:59 Asia/Dhaka',
756 - 'url' => Xpo::generate_utm_link(
757 - array(
758 - 'utmKey' => 'surprise_sale',
759 - )
760 - ),
761 - 'visibility' => ! Xpo::is_lc_active(),
762 - 'content_heading' => __( 'Surprise Sale:', 'ultimate-post' ),
763 - 'content_subheading' => __( 'Enjoy %s on PostX Pro.', 'ultimate-post' ),
764 - 'discount_content' => ' up to 50% Off',
765 - 'brand_color' => '#0322ff',
766 - 'icon' => ULTP_URL . 'assets/img/dashboard_banner/flash_sale_50_discount_logo.png',
767 - 'button_text' => __( 'Upgrade Now!', 'ultimate-post' ),
768 - 'is_discount_logo' => true,
769 - 'border_color' => '#0322ff',
770 - ),
771 -
772 - // Massive Sale
773 - array(
774 - 'key' => 'ultp_dashboard_content_notice_massive_sale_brand_logo',
775 - 'start' => '2026-06-02 00:00 Asia/Dhaka',
776 - 'end' => '2026-06-10 23:59 Asia/Dhaka',
777 - 'url' => Xpo::generate_utm_link(
778 - array(
779 - 'utmKey' => 'massive_sale',
780 - )
781 - ),
782 - 'visibility' => ! Xpo::is_lc_active(),
783 - 'content_heading' => __( 'Massive Sale:', 'ultimate-post' ),
784 - 'content_subheading' => __( 'Enjoy %s on PostX Pro.', 'ultimate-post' ),
785 - 'discount_content' => ' up to 50% Off',
786 - 'brand_color' => '#0322ff',
787 - 'icon' => ULTP_URL . 'assets/img/dashboard_banner/brand_logo_round.svg',
788 - 'button_text' => __( 'Claim Your Discount!', 'ultimate-post' ),
789 - 'is_discount_logo' => true,
790 - 'border_color' => '#0322ff',
791 - ),
792 - array(
793 - 'key' => 'ultp_dashboard_content_notice_massive_sale_discount_logo',
794 - 'start' => '2026-06-11 00:00 Asia/Dhaka',
795 - 'end' => '2026-06-16 23:59 Asia/Dhaka',
796 - 'url' => Xpo::generate_utm_link(
797 - array(
798 - 'utmKey' => 'massive_sale',
799 - )
800 - ),
801 - 'visibility' => ! Xpo::is_lc_active(),
802 - 'content_heading' => __( 'Massive Sale:', 'ultimate-post' ),
803 - 'content_subheading' => __( 'Enjoy %s on PostX Pro.', 'ultimate-post' ),
804 - 'discount_content' => ' up to 50% Off',
805 - 'brand_color' => '#0322ff',
806 - 'icon' => ULTP_URL . 'assets/img/dashboard_banner/flash_sale_50_discount_logo.png',
807 - 'button_text' => __( 'Upgrade Now!', 'ultimate-post' ),
808 - 'is_discount_logo' => true,
809 - 'border_color' => '#0322ff',
810 - ),
811 -
812 - // Final Hours Sale
813 - array(
814 - 'key' => 'ultp_dashboard_content_notice_final_hours_sale_brand_logo',
815 - 'start' => '2026-06-21 00:00 Asia/Dhaka',
816 - 'end' => '2026-06-24 23:59 Asia/Dhaka',
817 - 'url' => Xpo::generate_utm_link(
818 - array(
819 - 'utmKey' => 'final_hour',
820 - )
821 - ),
822 - 'visibility' => ! Xpo::is_lc_active(),
823 - 'content_heading' => __( 'Final Hours Sale:', 'ultimate-post' ),
824 - 'content_subheading' => __( 'Enjoy %s on PostX Pro.', 'ultimate-post' ),
825 - 'discount_content' => ' up to 50% Off',
826 - 'brand_color' => '#0322ff',
827 - 'icon' => ULTP_URL . 'assets/img/dashboard_banner/brand_logo_round.svg',
828 - 'button_text' => __( 'Claim Your Discount!', 'ultimate-post' ),
829 - 'is_discount_logo' => true,
830 - 'border_color' => '#0322ff',
831 - ),
832 - array(
833 - 'key' => 'ultp_dashboard_content_notice_final_hours_sale_discount_logo',
834 - 'start' => '2026-06-25 00:00 Asia/Dhaka',
835 - 'end' => '2026-06-27 23:59 Asia/Dhaka',
836 - 'url' => Xpo::generate_utm_link(
837 - array(
838 - 'utmKey' => 'final_hour',
839 - )
840 - ),
841 - 'visibility' => ! Xpo::is_lc_active(),
842 - 'content_heading' => __( 'Final Hours Sale:', 'ultimate-post' ),
843 - 'content_subheading' => __( 'Enjoy %s on PostX Pro.', 'ultimate-post' ),
844 - 'discount_content' => ' up to 50% Off',
845 - 'brand_color' => '#0322ff',
846 - 'icon' => ULTP_URL . 'assets/img/dashboard_banner/flash_sale_50_discount_logo.png',
847 - 'button_text' => __( 'Upgrade Now!', 'ultimate-post' ),
848 - 'is_discount_logo' => true,
849 - 'border_color' => '#0322ff',
850 - ),
851 -
852 - );
853 -
854 - $ultp_db_nonce = wp_create_nonce( 'ultp-nonce' );
855 -
856 - foreach ( $content_notices as $key => $notice ) {
857 - $notice_key = isset( $notice['key'] ) ? $notice['key'] : $this->notice_version;
858 - if ( isset( $_GET['ultp_notice'] ) && $notice_key === $_GET['ultp_notice'] ) {
859 - continue;
860 - } else {
861 - $border_color = $notice['border_color'];
862 -
863 - $current_time = gmdate( 'U' );
864 - $notice_start = gmdate( 'U', strtotime( $notice['start'] ) );
865 - $notice_end = gmdate( 'U', strtotime( $notice['end'] ) );
866 - if ( $current_time >= $notice_start && $current_time <= $notice_end && $notice['visibility'] ) {
867 - $notice_transient = Xpo::get_transient_without_cache( 'ultp_get_pro_notice_' . $notice_key );
868 -
869 - if ( 'off' !== $notice_transient ) {
870 - if ( $return_bool ) { // Early return for Other plugin notice.
871 - return true;
872 - }
873 - $query_args = array(
874 - 'ultp_notice' => $notice_key,
875 - 'wpnonce' => $ultp_db_nonce,
876 - );
877 - if ( isset( $notice['repeat_interval'] ) && $notice['repeat_interval'] ) {
878 - $query_args['ultp_interval'] = $notice['repeat_interval'];
879 - }
880 -
881 - $url = isset( $notice['url'] ) ? $notice['url'] : Xpo::generate_utm_link(
882 - array(
883 - 'utmKey' => 'content_notice',
884 - )
885 - );
886 -
887 - ?>
888 -
889 - <style id="ultp-notice-css" type="text/css">
890 - .ultp-content-notice-wrapper {
891 - border: 1px solid #c3c4c7;
892 - border-left: 3px solid #037fff;
893 - margin: 15px 0 !important;
894 - display: flex;
895 - align-items: center;
896 - background-color: #eef0f4;
897 - width: 100%;
898 - padding: 10px 0;
899 - position: relative;
900 - box-sizing: border-box;
901 - }
902 -
903 - .ultp-content-notice-wrapper.notice {
904 - margin: 10px 0;
905 - width: calc(100% - 20px);
906 - }
907 -
908 - .wrap .ultp-content-notice-wrapper.notice {
909 - width: 100%;
910 - }
911 -
912 - .ultp-content-notice-icon {
913 - margin-left: 15px;
914 - }
915 -
916 - .ultp-content-notice-discout-icon {
917 - margin-left: 10px;
918 - }
919 -
920 - .ultp-content-notice-icon img {
921 - max-width: 42px;
922 - height: 70px;
923 - }
924 -
925 - .ultp-content-notice-discout-icon img {
926 - height: 70px;
927 - width: 70px;
928 - object-fit: contain;
929 - }
930 -
931 - .ultp-notice-content-wrapper {
932 - display: flex;
933 - flex-direction: column;
934 - gap: 8px;
935 - font-size: 14px;
936 - line-height: 20px;
937 - margin-left: 15px;
938 - }
939 -
940 - .ultp-content-notice-buttons {
941 - display: flex;
942 - align-items: center;
943 - gap: 15px;
944 - }
945 -
946 - .ultp-content-notice-btn {
947 - font-weight: 600;
948 - text-transform: uppercase !important;
949 - padding: 2px 10px !important;
950 - background-color: #0322ff;
951 - border: none !important;
952 - }
953 -
954 - .ultp-content-discount_btn {
955 - background-color: #ffffff;
956 - text-decoration: none;
957 - border: 1px solid #0322ff;
958 - padding: 5px 10px;
959 - border-radius: 5px;
960 - font-weight: 500;
961 - text-transform: uppercase;
962 - color: #0322ff !important;
963 - }
964 -
965 - .ultp-content-notice-close {
966 - position: absolute;
967 - right: 2px;
968 - top: 5px;
969 - text-decoration: none;
970 - color: #b6b6b6;
971 - font-family: dashicons;
972 - font-size: 16px;
973 - line-height: 20px;
974 - }
975 -
976 - .ultp-content-notice-close-icon {
977 - font-size: 14px;
978 - }
979 - </style>
980 - <div class="ultp-content-notice-wrapper notice data_collection_notice"
981 - style="border-left: 3px solid <?php echo esc_attr( $border_color ); ?>;"
982 - >
983 - <?php
984 - if ( $notice['is_discount_logo'] ) {
985 - ?>
986 - <div class="ultp-content-notice-discout-icon"> <img src="<?php echo esc_url( $notice['icon'] ); ?>"/> </div>
987 - <?php
988 - } else {
989 - ?>
990 - <div class="ultp-content-notice-icon"> <img src="<?php echo esc_url( $notice['icon'] ); ?>"/> </div>
991 - <?php
992 - }
993 - ?>
994 -
995 - <div class="ultp-notice-content-wrapper">
996 - <div class="">
997 - <strong><?php printf( esc_html( $notice['content_heading'] ) ); ?> </strong>
998 - <?php
999 - printf(
1000 - wp_kses_post( $notice['content_subheading'] ),
1001 - '<strong>' . esc_html( $notice['discount_content'] ) . '</strong>'
1002 - );
1003 - ?>
1004 - </div>
1005 - <div class="ultp-content-notice-buttons">
1006 - <?php if ( isset( $notice['is_discount_logo'] ) && $notice['is_discount_logo'] ) : ?>
1007 - <a class="ultp-content-discount_btn" href="<?php echo esc_url( $url ); ?>" target="_blank">
1008 - <?php echo esc_html( $notice['button_text'] ); ?>
1009 - </a>
1010 - <?php else : ?>
1011 - <a class="ultp-content-notice-btn button button-primary" href="<?php echo esc_url( $url ); ?>" target="_blank" style="background-color: <?php echo ! empty( $notice['background_color'] ) ? esc_attr( $notice['background_color'] ) : '#86a62c'; ?>;">
1012 - <?php echo esc_html( $notice['button_text'] ); ?>
1013 -
1014 - </a>
1015 - <?php endif; ?>
1016 - </div>
1017 - </div>
1018 - <a href=
1019 - <?php
1020 - echo esc_url(
1021 - add_query_arg(
1022 - $query_args
1023 - )
1024 - );
1025 - ?>
1026 - class="ultp-content-notice-close"><span class="ultp-content-notice-close-icon dashicons dashicons-dismiss"> </span></a>
1027 - </div>
1028 - <?php
1029 - }
1030 - }
1031 - }
1032 - }
293 + return $this->render_campaigns( Content_Notice::class, $return_bool );
1033 294 }
1034 295
1035 296 /**
1036 297 * Handle Plugin Notice for all plugins
@@ -1038,9 +299,9 @@
1038 299 * @param array $active_lists Lists of all active plugin notice.
1039 300 * @return array
1040 301 */
1041 302 public function handle_xpo_active_notice_lists( $active_lists ) {
1042 - if ( $this->ultp_dashboard_banner_notice( true ) || $this->ultp_dashboard_content_notice( true ) ) {
303 + if ( ! empty( $this->get_active_campaigns() ) ) {
1043 304 $active_lists[ $this->plugin_notice_priority_key ] = $this->plugin_notice_priority;
1044 305 }
1045 306 return $active_lists;
1046 307 }
@@ -1056,6 +317,111 @@
1056 317 return true;
1057 318 }
1058 319 asort( $active_notices );
1059 320 return array_key_first( $active_notices ) === $this->plugin_notice_priority_key;
321 + }
322 +
323 + /**
324 + * Dashboard Image Banner Notice
325 + *
326 + * @param bool $return_bool True to only report whether an image banner would show.
327 + * @return bool|void
328 + */
329 + public function ultp_dashboard_image_banner_notice( $return_bool = false ) {
330 + return $this->render_campaigns( Image_Notice::class, $return_bool );
331 + }
332 +
333 + /**
334 + * Campaigns that should show on this request, in display order.
335 + *
336 + * Worked out once per request: the xpo_active_notice_lists filter and the render pass both need it,
337 + * and each campaign's dismissal check is an uncached database read.
338 + *
339 + * @param string $type Optional class name to keep only one kind of campaign.
340 + * @return Abstract_Campaign_Notice[]
341 + */
342 + private function get_active_campaigns( $type = '' ) {
343 + if ( null === $this->active_campaigns ) {
344 + $this->active_campaigns = array();
345 +
346 + // The registry is the file edited for every sale. Load it here, not at the top, so a
347 + // mistake in it (even a syntax error) hides the campaigns instead of breaking wp-admin.
348 + try {
349 + require_once __DIR__ . '/class-campaigns.php';
350 + $campaigns = Campaigns::all();
351 + } catch ( \Throwable $e ) {
352 + self::log_error( $e );
353 + $campaigns = array();
354 + }
355 +
356 + foreach ( $campaigns as $campaign ) {
357 + try {
358 + if ( $campaign instanceof Abstract_Campaign_Notice && $campaign->should_display() ) {
359 + $this->active_campaigns[] = $campaign;
360 + }
361 + } catch ( \Throwable $e ) {
362 + self::log_error( $e );
363 + }
364 + }
365 + }
366 +
367 + if ( '' === $type ) {
368 + return $this->active_campaigns;
369 + }
370 + return array_values(
371 + array_filter(
372 + $this->active_campaigns,
373 + function ( $campaign ) use ( $type ) {
374 + return $campaign instanceof $type;
375 + }
376 + )
377 + );
378 + }
379 +
380 + /**
381 + * Print every live campaign of one kind, or report whether there is one.
382 + *
383 + * @param string $type Campaign class name.
384 + * @param bool $return_bool True to only report whether one would show.
385 + * @return bool|void
386 + */
387 + private function render_campaigns( $type, $return_bool ) {
388 + $campaigns = $this->get_active_campaigns( $type );
389 +
390 + if ( $return_bool ) { // Early return for Other plugin notice.
391 + return ! empty( $campaigns );
392 + }
393 +
394 + foreach ( $campaigns as $campaign ) {
395 + $this->render_safely( $campaign );
396 + }
397 + }
398 +
399 + /**
400 + * Print one notice. It's buffered, so a notice that fails halfway leaves no broken markup behind.
401 + *
402 + * @param Notice_Type $notice Notice to print.
403 + * @return void
404 + */
405 + private function render_safely( Notice_Type $notice ) {
406 + ob_start();
407 + try {
408 + $notice->render();
409 + ob_end_flush();
410 + } catch ( \Throwable $e ) {
411 + ob_end_clean();
412 + self::log_error( $e );
413 + }
414 + }
415 +
416 + /**
417 + * Record a notice failure when debugging. Notices are never worth breaking wp-admin for.
418 + *
419 + * @param \Throwable $e What went wrong.
420 + * @return void
421 + */
422 + private static function log_error( \Throwable $e ) {
423 + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
424 + error_log( 'PostX notice: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only.
425 + }
1060 426 }
1061 427 }