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