PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 9.1.3
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v9.1.3
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.1.3, at legacy/admin/classes/class-wpvr-onboarding-notice.php

386 lines 13.3 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 * Check if a demo tour currently exists on the site.
168 *
169 * @return bool
170 */
171 private function has_demo_tour() {
172 $demo_tours = get_posts( array(
173 'post_type' => 'wpvr_item',
174 'post_status' => array( 'publish', 'draft', 'pending', 'private', 'future' ),
175 'posts_per_page' => 1,
176 'fields' => 'ids',
177 'meta_query' => array(
178 array(
179 'key' => 'wpvr_is_demo_tour',
180 'value' => '1',
181 'compare' => '=',
182 ),
183 ),
184 ) );
185
186 return ! empty( $demo_tours );
187 }
188
189 /* -----------------------------------------------------------------------
190 * Rendering
191 * --------------------------------------------------------------------- */
192
193 /**
194 * Output notice styles before the admin page body to prevent a layout flash.
195 */
196 public function render_styles() {
197 ?>
198 <style id="wpvr-onboarding-notice-styles">
199 #wpvr-onboarding-notice.wpvr-onboarding-notice {
200 box-sizing: border-box;
201 width: 100%;
202 border-left: 4px solid #3F04FE;
203 background: #fff;
204 padding: 0;
205 margin: 16px 0 0;
206 border-radius: 0 6px 6px 0;
207 box-shadow: 0 2px 8px rgba(63,4,254,.08);
208 }
209 #wpvr-onboarding-notice .wpvr-onboarding-notice__inner {
210 box-sizing: border-box;
211 display: flex;
212 align-items: center;
213 justify-content: space-between;
214 gap: 14px;
215 min-height: 68px;
216 padding: 14px 18px;
217 }
218 #wpvr-onboarding-notice .wpvr-onboarding-notice__body {
219 flex: 1 1 auto;
220 min-width: 0;
221 }
222 #wpvr-onboarding-notice .wpvr-onboarding-notice__text {
223 margin: 0;
224 font-size: 14px;
225 font-weight: 500;
226 color: #1d2327;
227 line-height: 1.5;
228 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
229 }
230 #wpvr-onboarding-notice .wpvr-onboarding-notice__actions {
231 display: flex;
232 align-items: center;
233 gap: 12px;
234 flex: 0 0 auto;
235 margin-left: auto;
236 }
237 #wpvr-onboarding-notice .wpvr-onboarding-notice__cta.button-primary {
238 box-sizing: border-box;
239 display: inline-flex;
240 align-items: center;
241 justify-content: center;
242 min-height: 40px;
243 background: #3F04FE;
244 border-color: #3F04FE;
245 color: #fff;
246 font-weight: 600;
247 padding: 0 16px;
248 line-height: 1.5;
249 border-radius: 5px;
250 text-decoration: none;
251 transition: background .15s ease, box-shadow .15s ease;
252 }
253 #wpvr-onboarding-notice .wpvr-onboarding-notice__cta.button-primary:hover,
254 #wpvr-onboarding-notice .wpvr-onboarding-notice__cta.button-primary:focus {
255 background: #2b00cc;
256 border-color: #2b00cc;
257 color: #fff;
258 box-shadow: 0 2px 8px rgba(63,4,254,.3);
259 outline: none;
260 }
261 #wpvr-onboarding-notice .wpvr-onboarding-notice__dismiss {
262 box-sizing: border-box;
263 display: inline-flex;
264 align-items: center;
265 justify-content: center;
266 min-height: 40px;
267 background: none;
268 border: 1px solid #d1d5db;
269 border-radius: 4px;
270 padding: 0 12px;
271 cursor: pointer;
272 font-size: 13px;
273 line-height: 1.5;
274 color: #6b7280;
275 font-family: inherit;
276 transition: border-color .15s ease, color .15s ease;
277 }
278 #wpvr-onboarding-notice .wpvr-onboarding-notice__dismiss:hover {
279 border-color: #9ca3af;
280 color: #374151;
281 }
282 @media screen and (max-width: 782px) {
283 #wpvr-onboarding-notice .wpvr-onboarding-notice__inner {
284 align-items: flex-start;
285 flex-direction: column;
286 }
287 #wpvr-onboarding-notice .wpvr-onboarding-notice__actions {
288 width: 100%;
289 margin-left: 0;
290 }
291 }
292 </style>
293 <?php
294 }
295
296 /**
297 * Output the notice HTML and inline JS.
298 */
299 public function render_notice() {
300 if ( ! $this->should_show() ) {
301 return;
302 }
303
304 $has_demo = $this->has_demo_tour();
305
306 if ( $has_demo ) {
307 $notice_text = __( "You're exploring with a demo tour. Ready to upload your own 360° photos and create your first real tour?", 'wpvr' );
308 $cta_text = __( 'Upload your 360 photo', 'wpvr' );
309 $cta_url = admin_url( 'post-new.php?post_type=wpvr_item' );
310 } else {
311 $notice_text = __( 'Welcome to WPVR! Our quick setup wizard will help you publish your first virtual tour in minutes.', 'wpvr' );
312 $cta_text = __( 'Start setup wizard', 'wpvr' );
313 $cta_url = admin_url( 'admin.php?page=rex-wpvr-setup-wizard' );
314 }
315
316 $nonce = wp_create_nonce( self::AJAX_DISMISS );
317 ?>
318 <div id="wpvr-onboarding-notice" class="wpvr-onboarding-notice notice">
319 <div class="wpvr-onboarding-notice__inner">
320 <div class="wpvr-onboarding-notice__body">
321 <p class="wpvr-onboarding-notice__text">
322 <?php echo esc_html( $notice_text ); ?>
323 </p>
324 </div>
325 <div class="wpvr-onboarding-notice__actions">
326 <a href="<?php echo esc_url( $cta_url ); ?>" class="button button-primary wpvr-onboarding-notice__cta" id="wpvr-onboarding-start-btn">
327 <?php echo esc_html( $cta_text ); ?>
328 </a>
329 <button type="button"
330 class="wpvr-onboarding-notice__dismiss"
331 id="wpvr-onboarding-dismiss-btn"
332 data-ajax="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>"
333 data-nonce="<?php echo esc_attr( $nonce ); ?>"
334 aria-label="<?php esc_attr_e( 'Dismiss this notice', 'wpvr' ); ?>">
335 <?php esc_html_e( 'Dismiss', 'wpvr' ); ?>
336 </button>
337 </div>
338 </div>
339 </div>
340
341 <script>
342 (function($) {
343 $(document).on('click', '#wpvr-onboarding-dismiss-btn', function() {
344 var $btn = $(this);
345 var $wrap = $('#wpvr-onboarding-notice');
346
347 // Immediately hide.
348 $wrap.fadeOut(250, function() { $wrap.remove(); });
349
350 // Permanently dismiss via AJAX.
351 $.post($btn.data('ajax'), {
352 action : '<?php echo esc_js( self::AJAX_DISMISS ); ?>',
353 nonce : $btn.data('nonce')
354 });
355 });
356 }(jQuery));
357 </script>
358 <?php
359 }
360
361 /* -----------------------------------------------------------------------
362 * AJAX handler
363 * --------------------------------------------------------------------- */
364
365 /**
366 * Handle the "Dismiss" AJAX request — permanently suppresses the notice.
367 */
368 public function handle_dismiss() {
369 if ( ! current_user_can( 'manage_options' ) ) {
370 wp_send_json_error( array( 'message' => 'Unauthorized' ), 403 );
371 return;
372 }
373
374 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
375 if ( ! wp_verify_nonce( $nonce, self::AJAX_DISMISS ) ) {
376 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
377 return;
378 }
379
380 // Permanent dismiss — no expiry.
381 update_option( self::OPT_DISMISSED, '1', false );
382
383 wp_send_json_success( array( 'message' => 'Dismissed' ) );
384 }
385 }
386