PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 9.0.2
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v9.0.2
9.1.3 9.1.2 9.1.1 9.1.0 9.0.3 9.0.2 9.0.1 9.0.0 8.5.79 8.5.78 8.5.77 8.5.76 8.5.75 8.5.74 8.5.73 8.5.72 8.5.71 8.5.70 8.5.69 8.5.68 8.5.35 8.5.36 8.5.37 8.5.38 8.5.39 All 222 releases
wpvr / legacy / admin / classes / class-wpvr-onboarding-notice.php

class-wpvr-onboarding-notice.php in WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress 9.0.2, at legacy/admin/classes/class-wpvr-onboarding-notice.php

352 lines 12.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WPVR Onboarding Notice
4 *
5 * Displays a notice on the tour listing page (edit-wpvr_item) reminding
6 * truly-new users that they can start the setup wizard.
7 *
8 * Visibility rules:
9 * 1. Only shown on the tour listing page (edit-wpvr_item).
10 * 2. Only shown to users who have NEVER created a real (non-demo) tour.
11 * Tracked via `wpvr_has_ever_created_tour` — set permanently via backfill
12 * or explicitly when the wizard AJAX creates a real tour.
13 * 3. NOT shown if the wizard was already completed (`wpvr_wizard_onboarding_done`).
14 * 4. NOT shown if the user already permanently dismissed this notice.
15 * 5. Demo tours are explicitly excluded from all checks.
16 *
17 * @package WPVR
18 * @since 7.4.83
19 */
20
21 if ( ! defined( 'ABSPATH' ) ) {
22 exit;
23 }
24
25 class WPVR_Onboarding_Notice {
26
27 /**
28 * Option: permanently set once a real (non-demo) wpvr_item is created.
29 */
30 const OPT_HAS_EVER_CREATED = 'wpvr_has_ever_created_tour';
31
32 /**
33 * Option: set when the wizard flow is completed (tour published via wizard).
34 */
35 const OPT_WIZARD_DONE = 'wpvr_wizard_onboarding_done';
36
37 /**
38 * Option: set permanently when user clicks "Dismiss".
39 */
40 const OPT_DISMISSED = 'wpvr_onboarding_notice_dismissed';
41
42 /**
43 * AJAX action name for the dismiss request.
44 */
45 const AJAX_DISMISS = 'wpvr_onboarding_notice_dismiss';
46
47 /**
48 * Bootstrap hooks.
49 */
50 public function __construct() {
51 // Backfill the permanent flag for existing installs (old customers).
52 // NOTE: We do NOT use wp_after_insert_post for demo-tour detection because
53 // `wpvr_is_demo_tour` meta is set AFTER wp_insert_post returns, making
54 // the timing unreliable. The backfill query handles it correctly instead.
55 add_action( 'admin_init', array( $this, 'backfill_ever_created_flag' ) );
56
57 // Register AJAX handler (logged-in users only).
58 add_action( 'wp_ajax_' . self::AJAX_DISMISS, array( $this, 'handle_dismiss' ) );
59
60 // Render the notice on admin_notices.
61 add_action( 'admin_head', array( $this, 'render_styles' ) );
62 add_action( 'admin_notices', array( $this, 'render_notice' ) );
63 }
64
65 /* -----------------------------------------------------------------------
66 * Backfill
67 * --------------------------------------------------------------------- */
68
69 /**
70 * One-time backfill: if the site already has ANY real (non-demo) wpvr_item
71 * posts (any status including trash), permanently set the flag so old
72 * customers are never shown the onboarding notice.
73 *
74 * Rate-limited to once per hour via transient. The transient is intentionally
75 * short so that a user who creates their first real tour sees the flag update
76 * quickly when they return to the listing page.
77 */
78 public function backfill_ever_created_flag() {
79 // Already flagged — bail.
80 if ( get_option( self::OPT_HAS_EVER_CREATED ) ) {
81 return;
82 }
83
84 // Rate-limit with an hourly transient.
85 if ( get_transient( 'wpvr_onb_backfill_check' ) ) {
86 return;
87 }
88 set_transient( 'wpvr_onb_backfill_check', 1, HOUR_IN_SECONDS );
89
90 // Exclude demo tours via meta_query.
91 $existing = get_posts( array(
92 'post_type' => 'wpvr_item',
93 'post_status' => array( 'publish', 'draft', 'pending', 'private', 'future', 'trash' ),
94 'posts_per_page' => 1,
95 'fields' => 'ids',
96 'meta_query' => array(
97 'relation' => 'OR',
98 array(
99 'key' => 'wpvr_is_demo_tour',
100 'compare' => 'NOT EXISTS',
101 ),
102 array(
103 'key' => 'wpvr_is_demo_tour',
104 'value' => '1',
105 'compare' => '!=',
106 ),
107 ),
108 ) );
109
110 if ( ! empty( $existing ) ) {
111 update_option( self::OPT_HAS_EVER_CREATED, '1', false );
112 }
113 }
114
115 /* -----------------------------------------------------------------------
116 * Visibility check
117 * --------------------------------------------------------------------- */
118
119 /**
120 * Whether the notice should be rendered.
121 *
122 * @return bool
123 */
124 private function should_show() {
125 // 1. Listing page only.
126 if ( ! $this->is_listing_page() ) {
127 return false;
128 }
129
130 // 2. Admins only.
131 if ( ! current_user_can( 'manage_options' ) ) {
132 return false;
133 }
134
135 // 3. If a real tour was ever created on this site → never show.
136 if ( get_option( self::OPT_HAS_EVER_CREATED ) ) {
137 return false;
138 }
139
140 // 4. If the wizard was completed → never show.
141 if ( get_option( self::OPT_WIZARD_DONE ) ) {
142 return false;
143 }
144
145 // 5. If permanently dismissed → never show.
146 if ( get_option( self::OPT_DISMISSED ) ) {
147 return false;
148 }
149
150 return true;
151 }
152
153 /**
154 * Check if current screen is the tour listing page.
155 *
156 * @return bool
157 */
158 private function is_listing_page() {
159 if ( ! is_admin() || ! function_exists( 'get_current_screen' ) ) {
160 return false;
161 }
162 $screen = get_current_screen();
163 return $screen && 'edit-wpvr_item' === $screen->id;
164 }
165
166 /* -----------------------------------------------------------------------
167 * Rendering
168 * --------------------------------------------------------------------- */
169
170 /**
171 * Output notice styles before the admin page body to prevent a layout flash.
172 */
173 public function render_styles() {
174 ?>
175 <style id="wpvr-onboarding-notice-styles">
176 #wpvr-onboarding-notice.wpvr-onboarding-notice {
177 box-sizing: border-box;
178 width: 100%;
179 border-left: 4px solid #3F04FE;
180 background: #fff;
181 padding: 0;
182 margin: 16px 0 0;
183 border-radius: 0 6px 6px 0;
184 box-shadow: 0 2px 8px rgba(63,4,254,.08);
185 }
186 #wpvr-onboarding-notice .wpvr-onboarding-notice__inner {
187 box-sizing: border-box;
188 display: flex;
189 align-items: center;
190 justify-content: space-between;
191 gap: 14px;
192 min-height: 68px;
193 padding: 14px 18px;
194 }
195 #wpvr-onboarding-notice .wpvr-onboarding-notice__body {
196 flex: 1 1 auto;
197 min-width: 0;
198 }
199 #wpvr-onboarding-notice .wpvr-onboarding-notice__text {
200 margin: 0;
201 font-size: 14px;
202 font-weight: 500;
203 color: #1d2327;
204 line-height: 1.5;
205 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
206 }
207 #wpvr-onboarding-notice .wpvr-onboarding-notice__actions {
208 display: flex;
209 align-items: center;
210 gap: 12px;
211 flex: 0 0 auto;
212 margin-left: auto;
213 }
214 #wpvr-onboarding-notice .wpvr-onboarding-notice__cta.button-primary {
215 box-sizing: border-box;
216 display: inline-flex;
217 align-items: center;
218 justify-content: center;
219 min-height: 40px;
220 background: #3F04FE;
221 border-color: #3F04FE;
222 color: #fff;
223 font-weight: 600;
224 padding: 0 16px;
225 line-height: 1.5;
226 border-radius: 5px;
227 text-decoration: none;
228 transition: background .15s ease, box-shadow .15s ease;
229 }
230 #wpvr-onboarding-notice .wpvr-onboarding-notice__cta.button-primary:hover,
231 #wpvr-onboarding-notice .wpvr-onboarding-notice__cta.button-primary:focus {
232 background: #2b00cc;
233 border-color: #2b00cc;
234 color: #fff;
235 box-shadow: 0 2px 8px rgba(63,4,254,.3);
236 outline: none;
237 }
238 #wpvr-onboarding-notice .wpvr-onboarding-notice__dismiss {
239 box-sizing: border-box;
240 display: inline-flex;
241 align-items: center;
242 justify-content: center;
243 min-height: 40px;
244 background: none;
245 border: 1px solid #d1d5db;
246 border-radius: 4px;
247 padding: 0 12px;
248 cursor: pointer;
249 font-size: 13px;
250 line-height: 1.5;
251 color: #6b7280;
252 font-family: inherit;
253 transition: border-color .15s ease, color .15s ease;
254 }
255 #wpvr-onboarding-notice .wpvr-onboarding-notice__dismiss:hover {
256 border-color: #9ca3af;
257 color: #374151;
258 }
259 @media screen and (max-width: 782px) {
260 #wpvr-onboarding-notice .wpvr-onboarding-notice__inner {
261 align-items: flex-start;
262 flex-direction: column;
263 }
264 #wpvr-onboarding-notice .wpvr-onboarding-notice__actions {
265 width: 100%;
266 margin-left: 0;
267 }
268 }
269 </style>
270 <?php
271 }
272
273 /**
274 * Output the notice HTML and inline JS.
275 */
276 public function render_notice() {
277 if ( ! $this->should_show() ) {
278 return;
279 }
280
281 $wizard_url = esc_url( admin_url( 'admin.php?page=rex-wpvr-setup-wizard' ) );
282 $nonce = wp_create_nonce( self::AJAX_DISMISS );
283 ?>
284 <div id="wpvr-onboarding-notice" class="wpvr-onboarding-notice notice">
285 <div class="wpvr-onboarding-notice__inner">
286 <div class="wpvr-onboarding-notice__body">
287 <p class="wpvr-onboarding-notice__text">
288 <?php esc_html_e( "You haven't created a virtual tour yet. Our quick setup wizard will help you publish your first tour in minutes!", 'wpvr' ); ?>
289 </p>
290 </div>
291 <div class="wpvr-onboarding-notice__actions">
292 <a href="<?php echo esc_url( $wizard_url ); ?>" class="button button-primary wpvr-onboarding-notice__cta" id="wpvr-onboarding-start-btn">
293 <?php esc_html_e( 'Start setup wizard', 'wpvr' ); ?>
294 </a>
295 <button type="button"
296 class="wpvr-onboarding-notice__dismiss"
297 id="wpvr-onboarding-dismiss-btn"
298 data-ajax="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>"
299 data-nonce="<?php echo esc_attr( $nonce ); ?>"
300 aria-label="<?php esc_attr_e( 'Dismiss this notice', 'wpvr' ); ?>">
301 <?php esc_html_e( 'Dismiss', 'wpvr' ); ?>
302 </button>
303 </div>
304 </div>
305 </div>
306
307 <script>
308 (function($) {
309 $(document).on('click', '#wpvr-onboarding-dismiss-btn', function() {
310 var $btn = $(this);
311 var $wrap = $('#wpvr-onboarding-notice');
312
313 // Immediately hide.
314 $wrap.fadeOut(250, function() { $wrap.remove(); });
315
316 // Permanently dismiss via AJAX.
317 $.post($btn.data('ajax'), {
318 action : '<?php echo esc_js( self::AJAX_DISMISS ); ?>',
319 nonce : $btn.data('nonce')
320 });
321 });
322 }(jQuery));
323 </script>
324 <?php
325 }
326
327 /* -----------------------------------------------------------------------
328 * AJAX handler
329 * --------------------------------------------------------------------- */
330
331 /**
332 * Handle the "Dismiss" AJAX request — permanently suppresses the notice.
333 */
334 public function handle_dismiss() {
335 if ( ! current_user_can( 'manage_options' ) ) {
336 wp_send_json_error( array( 'message' => 'Unauthorized' ), 403 );
337 return;
338 }
339
340 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
341 if ( ! wp_verify_nonce( $nonce, self::AJAX_DISMISS ) ) {
342 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
343 return;
344 }
345
346 // Permanent dismiss — no expiry.
347 update_option( self::OPT_DISMISSED, '1', false );
348
349 wp_send_json_success( array( 'message' => 'Dismissed' ) );
350 }
351 }
352