PluginProbe
SupportCandy – AI Customer Support Ticket System & Live Chatbot Agent / trunk
SupportCandy – AI Customer Support Ticket System & Live Chatbot Agent vtrunk
3.5.4 3.5.3 3.5.2 3.5.1 3.4.9 3.5.0 3.4.8 3.4.7 trunk 2.3.1 3.3.6 3.3.7 3.3.8 3.3.9 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.4.5 3.4.6
supportcandy / includes / admin / onboarding / class-wpsc-onboarding.php

class-wpsc-onboarding.php in SupportCandy – AI Customer Support Ticket System & Live Chatbot Agent trunk, at includes/admin/onboarding/class-wpsc-onboarding.php

506 lines 15.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 exit; // Exit if accessed directly!
4 }
5
6 if ( ! class_exists( 'WPSC_Onboarding' ) ) :
7
8 /**
9 * New-customer setup wizard shell.
10 *
11 * Every step is a small independent class under this folder (each one wrapping
12 * existing SupportCandy settings/APIs - no new settings mechanism is introduced).
13 * Setup must be completed - there is no way to skip a step or the wizard.
14 * Progress is tracked in a single option, 'wpsc-onboarding-wizard':
15 * array(
16 * 'status' => 'pending' | 'completed', // whole-wizard state.
17 * 'steps' => array( '<step-id>' => 'completed' ), // absent = pending.
18 * )
19 */
20 final class WPSC_Onboarding {
21
22 /**
23 * Whether the current admin screen is this wizard.
24 *
25 * @var boolean
26 */
27 public static $is_current_page = false;
28
29 /**
30 * Step to render first on this page load.
31 *
32 * @var string
33 */
34 public static $current_step = '';
35
36 /**
37 * Steps registry, keyed by step id.
38 *
39 * @var array
40 */
41 private static $steps = array();
42
43 /**
44 * Initialize this class
45 *
46 * @return void
47 */
48 public static function init() {
49
50 // Plug into the existing admin-menu extension points (same as any add-on).
51 add_filter( 'wpsc_admin_menu_slugs', array( __CLASS__, 'add_menu_slug' ) );
52 add_filter( 'wpsc_admin_submenus_data', array( __CLASS__, 'add_submenu' ) );
53
54 // While setup is pending, restrict the admin's own menu down to just the
55 // wizard - run last (PHP_INT_MAX) so this wins over every other slug any
56 // add-on registered on this same filter.
57 add_filter( 'wpsc_admin_menu_slugs', array( __CLASS__, 'restrict_menu_during_setup' ), PHP_INT_MAX );
58
59 add_action( 'admin_init', array( __CLASS__, 'detect_current_page' ), 1 );
60 add_action( 'admin_init', array( __CLASS__, 'maybe_redirect' ), 20 );
61 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
62 add_action( 'wpsc_js_ready', array( __CLASS__, 'register_js_ready_function' ) );
63 add_filter( 'wpsc_admin_localizations', array( __CLASS__, 'localizations' ) );
64
65 add_action( 'wp_ajax_wpsc_get_onboarding_step', array( __CLASS__, 'ajax_get_step' ) );
66 add_action( 'wp_ajax_wpsc_onboarding_complete_step', array( __CLASS__, 'ajax_complete_step' ) );
67 }
68
69 /**
70 * Steps registry
71 *
72 * @return array
73 */
74 public static function get_steps() {
75
76 if ( ! empty( self::$steps ) ) {
77 return self::$steps;
78 }
79
80 self::$steps = apply_filters(
81 'wpsc_onboarding_steps',
82 array(
83 'support-pages' => array(
84 'label' => esc_attr__( 'Setup', 'supportcandy' ),
85 'icon' => 'system',
86 'callback' => 'WPSC_Onboarding_Step_Pages',
87 ),
88 'ai-assistant' => array(
89 'label' => esc_attr__( 'AI Assistant', 'supportcandy' ),
90 'icon' => 'headset',
91 'callback' => 'WPSC_Onboarding_Step_AI',
92 ),
93 'categories' => array(
94 'label' => esc_attr__( 'Categories', 'supportcandy' ),
95 'icon' => 'subfolder',
96 'callback' => 'WPSC_Onboarding_Step_Categories',
97 ),
98 'statuses' => array(
99 'label' => esc_attr__( 'Statuses', 'supportcandy' ),
100 'icon' => 'gps-navigation',
101 'callback' => 'WPSC_Onboarding_Step_Statuses',
102 ),
103 'fields' => array(
104 'label' => esc_attr__( 'Fields', 'supportcandy' ),
105 'icon' => 'list-alt',
106 'callback' => 'WPSC_Onboarding_Step_Fields',
107 ),
108 'agents' => array(
109 'label' => esc_attr__( 'Agents', 'supportcandy' ),
110 'icon' => 'users',
111 'callback' => 'WPSC_Onboarding_Step_Agents',
112 ),
113 'emails' => array(
114 'label' => esc_attr__( 'Emails', 'supportcandy' ),
115 'icon' => 'envelope',
116 'callback' => 'WPSC_Onboarding_Step_Emails',
117 ),
118 'whats-next' => array(
119 'label' => esc_attr__( "What's Next", 'supportcandy' ),
120 'icon' => 'widget',
121 'callback' => 'WPSC_Onboarding_Step_Whats_Next',
122 ),
123 )
124 );
125
126 return self::$steps;
127 }
128
129 /**
130 * Add our menu slug to the allow-list consumed by WPSC_Admin::load_admin_menus().
131 *
132 * @param array $slugs - existing slugs.
133 * @return array
134 */
135 public static function add_menu_slug( $slugs ) {
136 $slugs[] = 'wpsc-onboarding';
137 return $slugs;
138 }
139
140 /**
141 * While setup is pending, only the wizard's own menu slug should be
142 * registered for a site admin - every other SupportCandy submenu (Tickets,
143 * Support Agents, Settings, add-on screens, etc.) stays hidden until they
144 * complete it. Scoped to admins only so agents (who
145 * cannot reach the wizard anyway) keep their normal Tickets/Archived menu.
146 *
147 * @param array $slugs - slugs registered so far.
148 * @return array
149 */
150 public static function restrict_menu_during_setup( $slugs ) {
151
152 if ( self::is_pending() && WPSC_Functions::is_site_admin() ) {
153 return array( 'wpsc-onboarding' );
154 }
155 return $slugs;
156 }
157
158 /**
159 * Register our submenu data the same way add-ons do - but only while
160 * setup is still pending. Once the admin has completed it,
161 * the menu item (and with it, the page itself, since nothing else
162 * registers this route) disappears.
163 *
164 * @param array $submenus - existing submenus.
165 * @return array
166 */
167 public static function add_submenu( $submenus ) {
168
169 if ( ! self::is_pending() ) {
170 return $submenus;
171 }
172
173 $submenus[] = array(
174 'parent_slug' => 'wpsc-tickets',
175 'page_title' => esc_attr__( 'Setup Wizard', 'supportcandy' ),
176 'menu_title' => esc_attr__( 'Setup Wizard', 'supportcandy' ),
177 'capability' => 'manage_options',
178 'menu_slug' => 'wpsc-onboarding',
179 'callback' => array( __CLASS__, 'layout' ),
180 );
181 return $submenus;
182 }
183
184 /**
185 * Detect whether we're on the wizard screen and which step to open.
186 *
187 * @return void
188 */
189 public static function detect_current_page() {
190
191 self::$is_current_page = isset( $_REQUEST['page'] ) && 'wpsc-onboarding' === $_REQUEST['page']; // phpcs:ignore
192
193 if ( ! self::$is_current_page ) {
194 return;
195 }
196
197 $steps = array_keys( self::get_steps() );
198 $requested = isset( $_REQUEST['step'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['step'] ) ) : ''; // phpcs:ignore
199
200 self::$current_step = in_array( $requested, $steps, true ) ? $requested : self::first_incomplete_step();
201 }
202
203 /**
204 * Redirect to the wizard once, right after a fresh install.
205 *
206 * @return void
207 */
208 public static function maybe_redirect() {
209
210 if ( self::$is_current_page || wp_doing_ajax() || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) {
211 return;
212 }
213
214 // Persistent guard: hiding the other submenus (restrict_menu_during_setup())
215 // doesn't stop the top-level "Support" menu link itself, which always
216 // points at the Tickets page regardless of which submenus are registered -
217 // so any other SupportCandy admin page is bounced back here while pending.
218 if ( self::is_pending() && WPSC_Functions::is_site_admin() && WPSC_Functions::is_wpsc_page() ) {
219 wp_safe_redirect( admin_url( 'admin.php?page=wpsc-onboarding' ) );
220 exit;
221 }
222
223 // One-time nudge right after a fresh install, even from a non-wpsc screen
224 // (e.g. the post-activation Plugins page).
225 if ( ! get_transient( 'wpsc_onboarding_redirect' ) ) {
226 return;
227 }
228
229 delete_transient( 'wpsc_onboarding_redirect' );
230
231 if ( ! WPSC_Functions::is_site_admin() || isset( $_GET['activate-multi'] ) ) { // phpcs:ignore
232 return;
233 }
234
235 wp_safe_redirect( admin_url( 'admin.php?page=wpsc-onboarding' ) );
236 exit;
237 }
238
239 /**
240 * Load JS/CSS for this screen only.
241 *
242 * @return void
243 */
244 public static function enqueue_scripts() {
245
246 if ( ! self::$is_current_page ) {
247 return;
248 }
249
250 wp_enqueue_script( 'wpsc-onboarding', WPSC_PLUGIN_URL . 'includes/admin/onboarding/onboarding.js', array( 'jquery', 'wpsc-admin', 'wpsc-framework' ), WPSC_VERSION, true );
251 wp_enqueue_style( 'wpsc-onboarding', WPSC_PLUGIN_URL . 'includes/admin/onboarding/onboarding.css', array( 'wpsc-framework' ), WPSC_VERSION );
252 }
253
254 /**
255 * Add localizations to local JS
256 *
257 * @param array $localizations - localization.
258 * @return array
259 */
260 public static function localizations( $localizations ) {
261
262 if ( ! self::$is_current_page ) {
263 return $localizations;
264 }
265
266 $localizations['onboarding_steps'] = array_keys( self::get_steps() );
267
268 return $localizations;
269 }
270
271 /**
272 * Register JS function to call on document ready so a direct/refreshed URL
273 * opens the right step.
274 *
275 * @return void
276 */
277 public static function register_js_ready_function() {
278
279 if ( ! self::$is_current_page ) {
280 return;
281 }
282 echo 'wpsc_onboarding_load_step(' . wp_json_encode( self::$current_step ) . ');' . PHP_EOL;
283 }
284
285 /**
286 * Current wizard state, merged with sane defaults.
287 *
288 * @return array
289 */
290 public static function get_state() {
291
292 $default = array(
293 'status' => 'pending',
294 'steps' => array(),
295 );
296 $state = get_option( 'wpsc-onboarding-wizard', $default );
297 return wp_parse_args( is_array( $state ) ? $state : array(), $default );
298 }
299
300 /**
301 * Whether the whole wizard is still pending (not yet completed).
302 * Drives both the menu restriction and the persistent redirect guard.
303 *
304 * @return boolean
305 */
306 public static function is_pending() {
307 return 'pending' === self::get_state()['status'];
308 }
309
310 /**
311 * Status of a single step: 'completed' or 'pending'.
312 *
313 * @param string $step_id - step id.
314 * @return string
315 */
316 public static function get_step_status( $step_id ) {
317 $state = self::get_state();
318 return isset( $state['steps'][ $step_id ] ) ? $state['steps'][ $step_id ] : 'pending';
319 }
320
321 /**
322 * Mark a single step complete.
323 *
324 * @param string $step_id - step id.
325 * @return void
326 */
327 public static function set_step_status( $step_id ) {
328
329 $state = self::get_state();
330 $state['steps'][ $step_id ] = 'completed';
331 update_option( 'wpsc-onboarding-wizard', $state );
332
333 // Reaching the last step marks the whole wizard done.
334 $ids = array_keys( self::get_steps() );
335 if ( end( $ids ) === $step_id ) {
336 $state['status'] = 'completed';
337 update_option( 'wpsc-onboarding-wizard', $state );
338 }
339 }
340
341 /**
342 * First step that isn't completed yet, or the last step if all are.
343 *
344 * @return string
345 */
346 public static function first_incomplete_step() {
347
348 $state = self::get_state();
349 foreach ( array_keys( self::get_steps() ) as $step_id ) {
350 if ( empty( $state['steps'][ $step_id ] ) ) {
351 return $step_id;
352 }
353 }
354 $ids = array_keys( self::get_steps() );
355 return end( $ids );
356 }
357
358 /**
359 * Shared continue footer used by most steps. Setup must be completed, so
360 * no step offers a way to skip past it.
361 *
362 * @param string $step_id - step id.
363 * @param boolean $is_last - whether this is the final step.
364 * @return void
365 */
366 public static function render_step_footer( $step_id, $is_last = false ) {
367 ?>
368 <div class="wpsc-onboarding-footer-actions">
369 <button
370 type="button"
371 class="wpsc-button normal primary"
372 onclick="wpsc_onboarding_complete_step('<?php echo esc_attr( $step_id ); ?>', '<?php echo esc_attr( wp_create_nonce( 'wpsc_onboarding_complete_step' ) ); ?>');">
373 <?php echo $is_last ? esc_attr__( 'Finish', 'supportcandy' ) : esc_attr__( 'Continue', 'supportcandy' ); ?>
374 </button>
375 </div>
376 <?php
377 }
378
379 /**
380 * Screen layout: header, step progress nav, and an empty body filled via AJAX.
381 *
382 * @return void
383 */
384 public static function layout() {
385
386 $steps = self::get_steps();
387 $current = self::$current_step ? self::$current_step : array_key_first( $steps );
388 ?>
389 <div class="wrap">
390 <hr class="wp-header-end">
391 <div id="wpsc-container" style="display:none;">
392 <div class="wpsc-onboarding-header">
393 <div class="wpsc-onboarding-title">
394 <h1><?php esc_attr_e( 'Setup Wizard', 'supportcandy' ); ?></h1>
395 <p><?php esc_attr_e( 'Complete these steps to get SupportCandy ready for your customers.', 'supportcandy' ); ?></p>
396 </div>
397 </div>
398 <div class="wpsc-onboarding-progress">
399 <?php
400 $i = 1;
401 foreach ( $steps as $step_id => $step ) :
402 $status = self::get_step_status( $step_id );
403 $classes = array( 'wpsc-onboarding-step-nav', $step_id, $status );
404 if ( $step_id === $current ) {
405 $classes[] = 'active';
406 }
407 ?>
408 <div
409 class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>"
410 onclick="wpsc_onboarding_load_step('<?php echo esc_attr( $step_id ); ?>');">
411 <span class="step-index">
412 <?php if ( 'completed' === $status ) : ?>
413 <?php WPSC_Icons::get( 'check' ); ?>
414 <?php else : ?>
415 <?php echo esc_html( $i ); ?>
416 <?php endif; ?>
417 </span>
418 <label><?php echo esc_attr( $step['label'] ); ?></label>
419 </div>
420 <?php
421 ++$i;
422 endforeach;
423 ?>
424 </div>
425 <div class="wpsc-onboarding-body"></div>
426 </div>
427 </div>
428 <?php
429 }
430
431 /**
432 * AJAX: render a step's body.
433 *
434 * @return void
435 */
436 public static function ajax_get_step() {
437
438 if ( ! WPSC_Functions::is_site_admin() ) {
439 wp_send_json_error( __( 'Unauthorized access!', 'supportcandy' ), 401 );
440 }
441
442 $steps = self::get_steps();
443 $step_id = isset( $_POST['step'] ) ? sanitize_text_field( wp_unslash( $_POST['step'] ) ) : ''; // phpcs:ignore
444
445 if ( ! isset( $steps[ $step_id ] ) ) {
446 wp_send_json_error( __( 'Invalid step!', 'supportcandy' ), 400 );
447 }
448
449 call_user_func( array( $steps[ $step_id ]['callback'], 'render' ), $step_id );
450 }
451
452 /**
453 * AJAX: mark a step complete.
454 *
455 * @return void
456 */
457 public static function ajax_complete_step() {
458
459 if ( check_ajax_referer( 'wpsc_onboarding_complete_step', '_ajax_nonce', false ) != 1 ) {
460 wp_send_json_error( 'Unauthorized request!', 401 );
461 }
462 if ( ! WPSC_Functions::is_site_admin() ) {
463 wp_send_json_error( __( 'Unauthorized access!', 'supportcandy' ), 401 );
464 }
465
466 $steps = self::get_steps();
467 $step_id = isset( $_POST['step'] ) ? sanitize_text_field( wp_unslash( $_POST['step'] ) ) : ''; // phpcs:ignore
468
469 if ( ! isset( $steps[ $step_id ] ) ) {
470 wp_send_json_error( __( 'Invalid step!', 'supportcandy' ), 400 );
471 }
472
473 // Steps must be completed in order. The progress nav lets an admin
474 // jump straight to any step (e.g. to review one already done), so
475 // without this check, clicking "Finish" on the last step could mark
476 // the whole wizard complete while an earlier required step - like
477 // the Support Page below - was never actually done.
478 $ids = array_keys( $steps );
479 $target_index = array_search( $step_id, $ids, true );
480 for ( $i = 0; $i < $target_index; $i++ ) {
481 if ( 'completed' !== self::get_step_status( $ids[ $i ] ) ) {
482 wp_send_json_error(
483 array(
484 'message' => __( 'Please complete the previous steps first.', 'supportcandy' ),
485 'redirect_step' => $ids[ $i ],
486 ),
487 400
488 );
489 }
490 }
491
492 // Setup must be completed, so the Support Page is a hard requirement
493 // before this (first) step can be marked done - mirrors the same
494 // check the step's own render() uses to gate its Continue button.
495 if ( 'support-pages' === $step_id && ! WPSC_Onboarding_Step_Pages::is_support_page_configured() ) {
496 wp_send_json_error( __( 'Please create the Support Page before continuing.', 'supportcandy' ), 400 );
497 }
498
499 self::set_step_status( $step_id );
500 wp_send_json_success();
501 }
502 }
503 endif;
504
505 WPSC_Onboarding::init();
506