PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 8.5.79
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v8.5.79
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 8.5.4 All 221 releases
wpvr / admin / classes / class-wpvr-onboarding-notice.php

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

326 lines 11.7 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_notices', array( $this, 'render_notice' ) );
62 }
63
64 /* -----------------------------------------------------------------------
65 * Backfill
66 * --------------------------------------------------------------------- */
67
68 /**
69 * One-time backfill: if the site already has ANY real (non-demo) wpvr_item
70 * posts (any status including trash), permanently set the flag so old
71 * customers are never shown the onboarding notice.
72 *
73 * Rate-limited to once per hour via transient. The transient is intentionally
74 * short so that a user who creates their first real tour sees the flag update
75 * quickly when they return to the listing page.
76 */
77 public function backfill_ever_created_flag() {
78 // Already flagged — bail.
79 if ( get_option( self::OPT_HAS_EVER_CREATED ) ) {
80 return;
81 }
82
83 // Rate-limit with an hourly transient.
84 if ( get_transient( 'wpvr_onb_backfill_check' ) ) {
85 return;
86 }
87 set_transient( 'wpvr_onb_backfill_check', 1, HOUR_IN_SECONDS );
88
89 // Exclude demo tours via meta_query.
90 $existing = get_posts( array(
91 'post_type' => 'wpvr_item',
92 'post_status' => array( 'publish', 'draft', 'pending', 'private', 'future', 'trash' ),
93 'posts_per_page' => 1,
94 'fields' => 'ids',
95 'meta_query' => array(
96 'relation' => 'OR',
97 array(
98 'key' => 'wpvr_is_demo_tour',
99 'compare' => 'NOT EXISTS',
100 ),
101 array(
102 'key' => 'wpvr_is_demo_tour',
103 'value' => '1',
104 'compare' => '!=',
105 ),
106 ),
107 ) );
108
109 if ( ! empty( $existing ) ) {
110 update_option( self::OPT_HAS_EVER_CREATED, '1', false );
111 }
112 }
113
114 /* -----------------------------------------------------------------------
115 * Visibility check
116 * --------------------------------------------------------------------- */
117
118 /**
119 * Whether the notice should be rendered.
120 *
121 * @return bool
122 */
123 private function should_show() {
124 // 1. Listing page only.
125 if ( ! $this->is_listing_page() ) {
126 return false;
127 }
128
129 // 2. Admins only.
130 if ( ! current_user_can( 'manage_options' ) ) {
131 return false;
132 }
133
134 // 3. If a real tour was ever created on this site → never show.
135 if ( get_option( self::OPT_HAS_EVER_CREATED ) ) {
136 return false;
137 }
138
139 // 4. If the wizard was completed → never show.
140 if ( get_option( self::OPT_WIZARD_DONE ) ) {
141 return false;
142 }
143
144 // 5. If permanently dismissed → never show.
145 if ( get_option( self::OPT_DISMISSED ) ) {
146 return false;
147 }
148
149 return true;
150 }
151
152 /**
153 * Check if current screen is the tour listing page.
154 *
155 * @return bool
156 */
157 private function is_listing_page() {
158 if ( ! is_admin() || ! function_exists( 'get_current_screen' ) ) {
159 return false;
160 }
161 $screen = get_current_screen();
162 return $screen && 'edit-wpvr_item' === $screen->id;
163 }
164
165 /* -----------------------------------------------------------------------
166 * Rendering
167 * --------------------------------------------------------------------- */
168
169 /**
170 * Output the notice HTML + inline CSS + inline JS.
171 */
172 public function render_notice() {
173 if ( ! $this->should_show() ) {
174 return;
175 }
176
177 $wizard_url = esc_url( admin_url( 'admin.php?page=rex-wpvr-setup-wizard' ) );
178 $nonce = wp_create_nonce( self::AJAX_DISMISS );
179 ?>
180 <div id="wpvr-onboarding-notice" class="wpvr-onboarding-notice notice">
181 <div class="wpvr-onboarding-notice__inner">
182 <div class="wpvr-onboarding-notice__icon" aria-hidden="true">
183 <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
184 <circle cx="14" cy="14" r="14" fill="#EEF0FF"/>
185 <path d="M14 7C11.239 7 9 9.239 9 12c0 2.209 1.343 4.1 3.25 4.934V19h1.5v-2.066A4.999 4.999 0 0 0 19 12c0-2.761-2.239-5-5-5Zm0 8.5A3.504 3.504 0 0 1 10.5 12 3.504 3.504 0 0 1 14 8.5 3.504 3.504 0 0 1 17.5 12 3.504 3.504 0 0 1 14 15.5ZM13.25 20h1.5v1h-1.5z" fill="#3F04FE"/>
186 </svg>
187 </div>
188 <div class="wpvr-onboarding-notice__body">
189 <p class="wpvr-onboarding-notice__text">
190 <?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' ); ?>
191 </p>
192 </div>
193 <div class="wpvr-onboarding-notice__actions">
194 <a href="<?php echo esc_url( $wizard_url ); ?>" class="button button-primary wpvr-onboarding-notice__cta" id="wpvr-onboarding-start-btn">
195 <?php esc_html_e( 'Start Onboarding', 'wpvr' ); ?>
196 </a>
197 <button type="button"
198 class="wpvr-onboarding-notice__dismiss"
199 id="wpvr-onboarding-dismiss-btn"
200 data-ajax="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>"
201 data-nonce="<?php echo esc_attr( $nonce ); ?>"
202 aria-label="<?php esc_attr_e( 'Dismiss this notice', 'wpvr' ); ?>">
203 <?php esc_html_e( 'Dismiss', 'wpvr' ); ?>
204 </button>
205 </div>
206 </div>
207 </div>
208
209 <style id="wpvr-onboarding-notice-styles">
210 #wpvr-onboarding-notice.wpvr-onboarding-notice {
211 border-left: 4px solid #3F04FE;
212 background: #fff;
213 padding: 0;
214 margin: 16px 20px 0 0;
215 border-radius: 0 6px 6px 0;
216 box-shadow: 0 2px 8px rgba(63,4,254,.08);
217 }
218 .wpvr-onboarding-notice__inner {
219 display: flex;
220 align-items: center;
221 gap: 14px;
222 padding: 14px 18px;
223 }
224 .wpvr-onboarding-notice__icon { flex-shrink: 0; }
225 .wpvr-onboarding-notice__body { flex: 1; }
226 .wpvr-onboarding-notice__text {
227 margin: 0;
228 font-size: 14px;
229 font-weight: 500;
230 color: #1e1b4b;
231 line-height: 1.5;
232 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
233 }
234 .wpvr-onboarding-notice__actions {
235 display: flex;
236 align-items: center;
237 gap: 12px;
238 flex-shrink: 0;
239 }
240 .wpvr-onboarding-notice__cta.button-primary {
241 background: #3F04FE;
242 border-color: #3F04FE;
243 color: #fff;
244 font-weight: 600;
245 padding: 6px 16px;
246 height: auto;
247 line-height: 1.5;
248 border-radius: 5px;
249 text-decoration: none;
250 transition: background .15s ease, box-shadow .15s ease;
251 }
252 .wpvr-onboarding-notice__cta.button-primary:hover,
253 .wpvr-onboarding-notice__cta.button-primary:focus {
254 background: #2b00cc;
255 border-color: #2b00cc;
256 color: #fff;
257 box-shadow: 0 2px 8px rgba(63,4,254,.3);
258 outline: none;
259 }
260 .wpvr-onboarding-notice__dismiss {
261 background: none;
262 border: 1px solid #d1d5db;
263 border-radius: 4px;
264 padding: 5px 12px;
265 cursor: pointer;
266 font-size: 13px;
267 color: #6b7280;
268 font-family: inherit;
269 transition: border-color .15s ease, color .15s ease;
270 }
271 .wpvr-onboarding-notice__dismiss:hover {
272 border-color: #9ca3af;
273 color: #374151;
274 }
275 @media screen and (max-width: 782px) {
276 .wpvr-onboarding-notice__inner { flex-wrap: wrap; }
277 .wpvr-onboarding-notice__actions { width: 100%; }
278 }
279 </style>
280
281 <script>
282 (function($) {
283 $(document).on('click', '#wpvr-onboarding-dismiss-btn', function() {
284 var $btn = $(this);
285 var $wrap = $('#wpvr-onboarding-notice');
286
287 // Immediately hide.
288 $wrap.fadeOut(250, function() { $wrap.remove(); });
289
290 // Permanently dismiss via AJAX.
291 $.post($btn.data('ajax'), {
292 action : '<?php echo esc_js( self::AJAX_DISMISS ); ?>',
293 nonce : $btn.data('nonce')
294 });
295 });
296 }(jQuery));
297 </script>
298 <?php
299 }
300
301 /* -----------------------------------------------------------------------
302 * AJAX handler
303 * --------------------------------------------------------------------- */
304
305 /**
306 * Handle the "Dismiss" AJAX request — permanently suppresses the notice.
307 */
308 public function handle_dismiss() {
309 if ( ! current_user_can( 'manage_options' ) ) {
310 wp_send_json_error( array( 'message' => 'Unauthorized' ), 403 );
311 return;
312 }
313
314 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
315 if ( ! wp_verify_nonce( $nonce, self::AJAX_DISMISS ) ) {
316 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
317 return;
318 }
319
320 // Permanent dismiss — no expiry.
321 update_option( self::OPT_DISMISSED, '1', false );
322
323 wp_send_json_success( array( 'message' => 'Dismissed' ) );
324 }
325 }
326