# xspeed/1.0.3/includes/class-onboarding.php

xSpeed Cache: AI-Powered Performance Hub with MCP, Caching &amp; CDN, version 1.0.3. 276 lines.

- Page: https://pluginprobe.com/plugins/xspeed/1.0.3/code/includes/class-onboarding.php
- Raw: https://pluginprobe.com/plugins/xspeed/1.0.3/raw/includes/class-onboarding.php
- Modified: 2026-06-01T17:33:22+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/xspeed/1.0.3/code/includes/class-onboarding.php#L10-L20`.

```php
<?php
/**
 * First-run onboarding wizard.
 *
 * Owns:
 *   - The hidden submenu page (`xspeed-onboarding`) and its single root <div>.
 *   - The activation-triggered redirect to that page.
 *   - The completion flag.
 *   - The environment-check payload + REST routes (`/onboarding/apply`,
 *     `/onboarding/complete`).
 *
 * Behavior contract is documented in DESIGN.md §25.
 *
 * @package XSpeed
 */

namespace XSpeed;

defined( 'ABSPATH' ) || exit;

class Onboarding {

	const PAGE_SLUG       = 'xspeed-onboarding';
	const OPT_REDIRECT    = 'xspeed_redirect_to_onboarding';
	const OPT_COMPLETE    = 'xspeed_onboarding_complete';

	public function __construct() {
		add_action( 'admin_menu', array( $this, 'register_menu' ), 20 );
		add_action( 'admin_init', array( $this, 'maybe_redirect' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
	}

	/**
	 * Mark the next admin_init to redirect this user to the wizard. Called
	 * from Plugin::activate(). Idempotent.
	 */
	public static function flag_redirect() {
		update_option( self::OPT_REDIRECT, 1, false );
	}

	public static function is_complete() {
		return (bool) get_option( self::OPT_COMPLETE, 0 );
	}

	public function register_menu() {
		// Visible "Setup Wizard" submenu under xSpeed. Stays in the
		// menu even after onboarding is complete so users can re-run
		// the wizard any time (a fresh run sets
		// xspeed_onboarding_complete = false again on completion).
		add_submenu_page(
			Admin::PAGE_SLUG,
			__( 'xSpeed Setup Wizard', 'xspeed' ),
			__( 'Setup Wizard', 'xspeed' ),
			'manage_options',
			self::PAGE_SLUG,
			array( $this, 'render' )
		);
	}

	public function render() {
		// Reuse the dashboard's mount ID so the Tailwind `important` selector
		// keeps working (utilities are scoped to `#xspeed-app`). The host
		// class strips the dashboard-only fixed-height/flex shell so the
		// wizard can lay out as a centered card. See src/styles.css.
		$dark = 'dark' === Admin::user_theme() ? ' dark' : '';
		printf(
			'<div id="xspeed-app" class="xspeed-root xspeed-onboarding-host%s"></div>',
			esc_attr( $dark )
		);
	}

	/**
	 * Send the user to the wizard on the first admin_init after activation.
	 * Skipped for AJAX/REST/cron, bulk-activate flows, and when the wizard
	 * has already been completed.
	 */
	public function maybe_redirect() {
		if ( ! get_option( self::OPT_REDIRECT ) ) {
			return;
		}
		if ( wp_doing_ajax() || wp_doing_cron() ) {
			return;
		}
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing decision; we don't process form data here.
		if ( isset( $_GET['activate-multi'] ) ) {
			delete_option( self::OPT_REDIRECT );
			return;
		}
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		if ( self::is_complete() ) {
			delete_option( self::OPT_REDIRECT );
			return;
		}

		delete_option( self::OPT_REDIRECT );

		wp_safe_redirect( admin_url( 'admin.php?page=' . self::PAGE_SLUG ) );
		exit;
	}

	public function enqueue( $hook ) {
		// `add_submenu_page` registers this page's hook suffix as
		// "{parent_slug}_page_{slug}", but because we immediately call
		// `remove_submenu_page`, WordPress still loads it via the toplevel
		// page hook when the URL is hit directly. Match both shapes.
		$wanted = array(
			'admin_page_' . self::PAGE_SLUG,
			Admin::PAGE_SLUG . '_page_' . self::PAGE_SLUG,
			'toplevel_page_' . self::PAGE_SLUG,
		);
		if ( ! in_array( $hook, $wanted, true ) ) {
			return;
		}

		$asset_js  = XSPEED_DIR . 'assets/admin.js';
		$asset_css = XSPEED_DIR . 'assets/admin.css';

		if ( file_exists( $asset_js ) ) {
			wp_enqueue_script(
				'xspeed-admin',
				XSPEED_URL . 'assets/admin.js',
				array( 'wp-api-fetch' ),
				XSPEED_VERSION,
				true
			);
		}
		if ( file_exists( $asset_css ) ) {
			wp_enqueue_style(
				'xspeed-admin',
				XSPEED_URL . 'assets/admin.css',
				array(),
				XSPEED_VERSION
			);
		}

		wp_localize_script(
			'xspeed-admin',
			'XSpeedConfig',
			array(
				'mode'        => 'onboarding',
				'restUrl'     => esc_url_raw( rest_url( Rest_Api::NAMESPACE_V1 ) ),
				'nonce'       => wp_create_nonce( 'wp_rest' ),
				'version'     => XSPEED_VERSION,
				'dashboardUrl' => admin_url( 'admin.php?page=' . Admin::PAGE_SLUG ),
				'bootstrap'   => array(
					'settings' => Settings::get(),
					'env'      => self::env_payload(),
				),
			)
		);
	}

	/**
	 * Environment snapshot rendered as Step 1's health rows. Pure read —
	 * never writes to disk, never makes outbound requests.
	 */
	public static function env_payload() {
		// Single source of truth for environment checks lives in
		// XSpeed\Health. Both the Onboarding wizard and the Health
		// module's dashboard panel consume from there.
		return Health::env_payload();
	}

	public function register_routes() {
		register_rest_route(
			Rest_Api::NAMESPACE_V1,
			'/onboarding/apply',
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'apply' ),
				'permission_callback' => array( $this, 'permissions' ),
			)
		);
		register_rest_route(
			Rest_Api::NAMESPACE_V1,
			'/onboarding/complete',
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'complete' ),
				'permission_callback' => array( $this, 'permissions' ),
			)
		);
		register_rest_route(
			Rest_Api::NAMESPACE_V1,
			'/onboarding/reset',
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'reset' ),
				'permission_callback' => array( $this, 'permissions' ),
			)
		);
	}

	public function permissions() {
		return current_user_can( 'manage_options' );
	}

	/**
	 * Apply the wizard's selected settings + flip the cache drop-in on if
	 * requested. Single REST round-trip so the wizard never lands in a
	 * half-applied state.
	 */
	public function apply( \WP_REST_Request $request ) {
		$params = $request->get_json_params();
		if ( ! is_array( $params ) ) {
			$params = $request->get_params();
		}

		$want_cache = ! empty( $params['cache_enabled'] );

		// Per-module settings go through Settings_Manager (the schema-
		// validated authority for each module). Legacy Settings::update
		// is reserved for fields still in xspeed_options (cache_expiry,
		// excluded_urls) until the Cache module migration lands.
		Settings_Manager::update(
			'minify',
			array(
				'minify_html' => ! empty( $params['minify_html'] ),
				'minify_css'  => ! empty( $params['minify_css'] ),
				'minify_js'   => ! empty( $params['minify_js'] ),
				'defer_js'    => ! empty( $params['defer_js'] ),
			)
		);
		Settings_Manager::update(
			'gzip',
			array(
				'gzip_enabled' => ! empty( $params['gzip_enabled'] ),
			)
		);
		Settings_Manager::update(
			'cache',
			array(
				'cache_expiry' => isset( $params['cache_expiry'] ) ? absint( $params['cache_expiry'] ) : 24,
			)
		);
		// New optional modules surfaced in the wizard — keys are
		// only updated when present in the payload so legacy
		// onboarding-complete sites aren't disturbed.
		if ( array_key_exists( 'lazy_images', $params ) ) {
			Settings_Manager::update(
				'lazy',
				array( 'lazy_images' => ! empty( $params['lazy_images'] ) )
			);
		}
		if ( array_key_exists( 'browser_cache', $params ) ) {
			Settings_Manager::update(
				'browser-cache',
				array( 'enabled' => ! empty( $params['browser_cache'] ) )
			);
		}

		$install_state = Cache::toggle( $want_cache );
		Settings::update( array( 'cache_enabled' => $install_state['enabled'] ) );

		return rest_ensure_response(
			array(
				'settings'      => Settings::get(),
				'install_state' => $install_state,
			)
		);
	}

	public function complete() {
		update_option( self::OPT_COMPLETE, 1, false );
		return rest_ensure_response( array( 'ok' => true ) );
	}

	public function reset() {
		delete_option( self::OPT_COMPLETE );
		return rest_ensure_response( array( 'ok' => true ) );
	}
}

```
