skyverge/wc-plugin-framework

View on GitHub
woocommerce/admin/abstract-sv-wc-plugin-admin-setup-wizard.php

Summary

Maintainability
F
4 days
Test Coverage
<?php
/**
 * WooCommerce Plugin Framework
 *
 * This source file is subject to the GNU General Public License v3.0
 * that is bundled with this package in the file license.txt.
 * It is also available through the world-wide-web at this URL:
 * http://www.gnu.org/licenses/gpl-3.0.html
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@skyverge.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade the plugin to newer
 * versions in the future. If you wish to customize the plugin for your
 * needs please refer to http://www.skyverge.com
 *
 * @author    SkyVerge
 * @copyright Copyright (c) 2013-2024, SkyVerge, Inc.
 * @license   http://www.gnu.org/licenses/gpl-3.0.html GNU General Public License v3.0
 */

namespace SkyVerge\WooCommerce\PluginFramework\v5_12_3\Admin;

defined( 'ABSPATH' ) or exit;

use SkyVerge\WooCommerce\PluginFramework\v5_12_3 as Framework;

if ( ! class_exists( '\\SkyVerge\\WooCommerce\\PluginFramework\\v5_12_3\\Admin\\Setup_Wizard' ) ) :


/**
 * The plugin Setup Wizard class.
 *
 * This creates a setup wizard so that plugins can provide a user-friendly
 * step-by-step interaction for configuring critical plugin options.
 *
 * Based on WooCommerce's \WC_Admin_Setup_Wizard
 *
 * @since 5.2.2
 */
#[\AllowDynamicProperties]
abstract class Setup_Wizard {


    /** the "finish" step ID */
    const ACTION_FINISH = 'finish';


    /** @var string the user capability required to use this wizard */
    protected $required_capability = 'manage_woocommerce';

    /** @var string the current step ID */
    protected $current_step = '';

    /** @var array registered steps to be displayed */
    protected $steps = array();

    /** @var string setup handler ID  */
    private $id;

    /** @var Framework\SV_WC_Plugin plugin instance */
    private $plugin;


    /**
     * Constructs the class.
     *
     * @param Framework\SV_WC_Plugin $plugin plugin instance
     */
    public function __construct( Framework\SV_WC_Plugin $plugin ) {

        // sanity check for admin and permissions
        if ( ! is_admin() || ! current_user_can( $this->required_capability ) ) {
            return;
        }

        $this->id     = $plugin->get_id();
        $this->plugin = $plugin;

        // register the steps
        $this->register_steps();

        /**
         * Filters the registered setup wizard steps.
         *
         * @since 5.2.2
         *
         * @param array $steps registered steps
         */
        $this->steps = apply_filters( "wc_{$this->id}_setup_wizard_steps", $this->steps, $this );

        // only continue if there are registered steps
        if ( $this->has_steps() ) {

            // if requesting the wizard
            if ( $this->is_setup_page() ) {

                $this->init_setup();

            // otherwise, add the hooks for customizing the regular admin
            } else {

                $this->add_hooks();

                // mark the wizard as complete if specifically requested
                if ( Framework\SV_WC_Helper::get_requested_value( "wc_{$this->id}_setup_wizard_complete" ) ) {
                    $this->complete_setup();
                }
            }
        }
    }


    /**
     * Registers the setup steps.
     *
     * Plugins should extend this to register their own steps.
     *
     * @since 5.2.2
     */
    abstract protected function register_steps();


    /**
     * Adds the action & filter hooks.
     *
     * @since 5.2.2
     */
    protected function add_hooks() {

        // add any admin notices
        add_action( 'admin_notices', array( $this, 'add_admin_notices' ) );

        // add a 'Setup' link to the plugin action links if the wizard hasn't been completed
        if ( ! $this->is_complete() ) {
            add_filter( 'plugin_action_links_' . plugin_basename( $this->get_plugin()->get_plugin_file() ), array( $this, 'add_setup_link' ), 20 );
        }
    }


    /**
     * Adds any admin notices.
     *
     * @since 5.2.2
     */
    public function add_admin_notices() {

        if ( Framework\SV_WC_Helper::is_current_screen( 'plugins' ) || $this->get_plugin()->is_plugin_settings() ) {

            if ( $this->is_complete() && $this->get_documentation_notice_message() ) {
                $notice_id = "wc_{$this->id}_docs";
                $message   = $this->get_documentation_notice_message();
            } else {
                $notice_id = "wc_{$this->id}_setup";
                $message   = $this->get_setup_notice_message();
            }

            $this->get_plugin()->get_admin_notice_handler()->add_admin_notice( $message, $notice_id, array(
                'always_show_on_settings' => false,
            ) );
        }
    }


    /**
     * Gets the new installation documentation notice message.
     *
     * This prompts users to read the docs and is displayed if the wizard has
     * already been completed.
     *
     * @since 5.2.2
     *
     * @return string
     */
    protected function get_documentation_notice_message() {

        if ( $this->get_plugin()->get_documentation_url() ) {

            $message = sprintf(
                /** translators: Placeholders: %1$s - plugin name, %2$s - <a> tag, %3$s - </a> tag */
                __( 'Thanks for installing %1$s! To get started, take a minute to %2$sread the documentation%3$s :)', 'woocommerce-plugin-framework' ),
                esc_html( $this->get_plugin()->get_plugin_name() ),
                '<a href="' . esc_url( $this->get_plugin()->get_documentation_url() )  . '" target="_blank">', '</a>'
            );

        } else {

            $message = '';
        }

        return $message;
    }


    /**
     * Gets the new installation setup notice message.
     *
     * This prompts users to start the setup wizard and is displayed if the
     * wizard has not yet been completed.
     *
     * @since 5.2.2
     *
     * @return string
     */
    protected function get_setup_notice_message() {

        return sprintf(
            /** translators: Placeholders: %1$s - plugin name, %2$s - <a> tag, %3$s - </a> tag */
            __( 'Thanks for installing %1$s! To get started, take a minute to complete these %2$squick and easy setup steps%3$s :)', 'woocommerce-plugin-framework' ),
            esc_html( $this->get_plugin()->get_plugin_name() ),
            '<a href="' . esc_url( $this->get_setup_url() )  . '">', '</a>'
        );
    }


    /**
     * Adds a 'Setup' link to the plugin action links if the wizard hasn't been completed.
     *
     * This will override the plugin's standard "Configure" link with a link to this setup wizard.
     *
     * @internal
     *
     * @since 5.2.2
     *
     * @param array $action_links plugin action links
     * @return array
     */
    public function add_setup_link( $action_links ) {

        // remove the standard plugin "Configure" link
        unset( $action_links['configure'] );

        $setup_link = array(
            'setup' => sprintf( '<a href="%s">%s</a>', $this->get_setup_url(), esc_html__( 'Setup', 'woocommerce-plugin-framework' ) ),
        );

        return array_merge( $setup_link, $action_links );
    }


    /**
     * Initializes setup.
     *
     * @since 5.2.2
     */
    protected function init_setup() {

        // get a step ID from $_GET
        $current_step   = sanitize_key( Framework\SV_WC_Helper::get_requested_value( 'step' ) );
        $current_action = sanitize_key( Framework\SV_WC_Helper::get_requested_value( 'action' ) );

        if ( ! $current_action ) {

            if ( $this->has_step( $current_step ) ) {
                $this->current_step = $current_step;
            } elseif ( $first_step_url = $this->get_step_url( key( $this->steps ) ) ) {
                wp_safe_redirect( $first_step_url );
                exit;
            } else {
                wp_safe_redirect( $this->get_dashboard_url() );
                exit;
            }
        }

        // add the page to WP core
        add_action( 'admin_menu', array( $this, 'add_page' ) );

        // renders the entire setup page markup
        add_action( 'admin_init', array( $this, 'render_page' ) );
    }


    /**
     * Adds the page to WordPress core.
     *
     * While this doesn't output any markup/menu items, it is essential to officially register the page to avoid permissions issues.
     *
     * @internal
     *
     * @since 5.2.2
     */
    public function add_page() {

        add_dashboard_page( '', '', $this->required_capability, $this->get_slug(), '' );
    }


    /**
     * Renders the entire setup page markup.
     *
     * @internal
     *
     * @since 5.2.2
     */
    public function render_page() {

        // maybe save and move onto the next step
        $error_message = Framework\SV_WC_Helper::get_posted_value( 'save_step' ) ? $this->save_step( $this->current_step ) : '';

        $page_title = sprintf(
            /* translators: Placeholders: %s - plugin name */
            __( '%s &rsaquo; Setup', 'woocommerce-plugin-framework' ),
            $this->get_plugin()->get_plugin_name()
        );

        // add the step name to the page title
        if ( ! empty( $this->steps[ $this->current_step ]['name'] ) ) {
            $page_title .= " &rsaquo; {$this->steps[ $this->current_step ]['name']}";
        }

        $this->load_scripts_styles();

        ob_start();

        ?>
        <!DOCTYPE html>
        <html <?php language_attributes(); ?>>
            <head>
                <meta name="viewport" content="width=device-width" />
                <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
                <title><?php echo esc_html( $page_title ); ?></title>
                <?php wp_print_scripts( 'wc-setup' ); ?>
                <?php do_action( 'admin_print_scripts' ); ?>
                <?php do_action( 'admin_print_styles' ); ?>
            </head>
            <body class="wc-setup wp-core-ui <?php echo esc_attr( $this->get_slug() ); ?>">
                <?php $this->render_header(); ?>
                <?php $this->render_steps(); ?>
                <?php $this->render_content( $error_message ); ?>
                <?php $this->render_footer(); ?>
            </body>
        </html>
        <?php

        exit;
    }


    /**
     * Saves a step.
     *
     * @since 5.2.2
     *
     * @param string $step_id the step ID being saved
     * @return void|string redirects upon success, returns an error message upon failure
     */
    protected function save_step( $step_id ) {

        $error_message = __( 'Oops! An error occurred, please try again.', 'woocommerce-plugin-framework' );

        try {

            // bail early if the nonce is bad
            if ( ! wp_verify_nonce( Framework\SV_WC_Helper::get_posted_value( 'nonce' ), "wc_{$this->id}_setup_wizard_save" ) ) {
                throw new Framework\SV_WC_Plugin_Exception( $error_message );
            }

            if ( $this->has_step( $step_id ) ) {

                // if the step has a saving callback defined, save the form fields
                if ( is_callable( $this->steps[ $step_id ]['save'] ) ) {
                    call_user_func( $this->steps[ $step_id ]['save'], $this );
                }

                // move to the next step
                wp_safe_redirect( $this->get_next_step_url( $step_id ) );
                exit;
            }

        } catch ( Framework\SV_WC_Plugin_Exception $exception ) {

            return $exception->getMessage() ? $exception->getMessage() : $error_message;
        }
    }


    /**
     * Registers and enqueues the wizard's scripts and styles.
     *
     * @since 5.2.2
     */
    protected function load_scripts_styles() {

        $fw_version = $this->get_plugin()->get_assets_version();
        $wc_version = defined( 'WC_VERSION' ) ? WC_VERSION : WC()->version ?? $fw_version;

        // block UI
        wp_register_script( 'jquery-blockui', WC()->plugin_url() . '/assets/js/jquery-blockui/jquery.blockUI.min.js', [ 'jquery' ], '2.70', true );

        // enhanced dropdowns
        wp_register_script( 'selectWoo', WC()->plugin_url() . '/assets/js/selectWoo/selectWoo.full.min.js', [ 'jquery' ], '1.0.0' );
        wp_register_script( 'wc-enhanced-select', WC()->plugin_url() . '/assets/js/admin/wc-enhanced-select.min.js', [ 'jquery', 'selectWoo' ], $wc_version );
        wp_localize_script(
            'wc-enhanced-select',
            'wc_enhanced_select_params',
            [
                'i18n_no_matches'           => _x( 'No matches found', 'enhanced select', 'woocommerce-plugin-framework' ),
                'i18n_ajax_error'           => _x( 'Loading failed', 'enhanced select', 'woocommerce-plugin-framework' ),
                'i18n_input_too_short_1'    => _x( 'Please enter 1 or more characters', 'enhanced select', 'woocommerce-plugin-framework' ),
                'i18n_input_too_short_n'    => _x( 'Please enter %qty% or more characters', 'enhanced select', 'woocommerce-plugin-framework' ),
                'i18n_input_too_long_1'     => _x( 'Please delete 1 character', 'enhanced select', 'woocommerce-plugin-framework' ),
                'i18n_input_too_long_n'     => _x( 'Please delete %qty% characters', 'enhanced select', 'woocommerce-plugin-framework' ),
                'i18n_selection_too_long_1' => _x( 'You can only select 1 item', 'enhanced select', 'woocommerce-plugin-framework' ),
                'i18n_selection_too_long_n' => _x( 'You can only select %qty% items', 'enhanced select', 'woocommerce-plugin-framework' ),
                'i18n_load_more'            => _x( 'Loading more results&hellip;', 'enhanced select', 'woocommerce-plugin-framework' ),
                'i18n_searching'            => _x( 'Searching&hellip;', 'enhanced select', 'woocommerce-plugin-framework' ),
                'ajax_url'                  => admin_url( 'admin-ajax.php' ),
                'search_products_nonce'     => wp_create_nonce( 'search-products' ),
                'search_customers_nonce'    => wp_create_nonce( 'search-customers' ),
            ]
        );

        // WooCommerce Setup core styles
        wp_enqueue_style( 'woocommerce_admin_styles', WC()->plugin_url() . '/assets/css/admin.css', [], $wc_version );
        wp_enqueue_style( 'wc-setup', WC()->plugin_url() . '/assets/css/wc-setup.css', [ 'dashicons', 'install' ], $wc_version );

        // framework bundled styles
        wp_enqueue_style( 'sv-wc-admin-setup', $this->get_plugin()->get_framework_assets_url() . '/css/admin/sv-wc-plugin-admin-setup-wizard.min.css', [ 'wc-setup' ], $fw_version );
        wp_enqueue_script( 'sv-wc-admin-setup', $this->get_plugin()->get_framework_assets_url() . '/js/admin/sv-wc-plugin-admin-setup-wizard.min.js', [ 'jquery', 'wc-enhanced-select', 'jquery-blockui' ], $fw_version );
    }


    /** Header Methods ************************************************************************************************/


    /**
     * Renders the header markup.
     *
     * @since 5.2.2
     */
    protected function render_header() {

        $title     = $this->get_plugin()->get_plugin_name();
        $link_url  = $this->get_plugin()->get_sales_page_url();
        $image_url = $this->get_header_image_url();

        $header_content = $image_url ? '<img src="' . esc_url( $image_url ) . '" alt="' . esc_attr( $title ) . '" />' : $title;

        ?>
        <h1 id="wc-logo" class="sv-wc-plugin-logo <?php echo esc_attr( 'wc-' . $this->get_plugin()->get_id_dasherized() . '-logo' ); ?>">
            <?php if ( $link_url ) : ?>
                <a href="<?php echo esc_url( $link_url ); ?>" target="_blank"><?php echo $header_content; ?></a>
            <?php else : ?>
                <?php echo esc_html( $header_content ); ?>
            <?php endif; ?>
        </h1>
        <?php
    }


    /**
     * Gets the header image URL.
     *
     * Plugins can override this to point to their own branding image URL.
     *
     * @since 5.2.2
     *
     * @return string
     */
    protected function get_header_image_url() {

        return '';
    }


    /**
     * Renders the step list.
     *
     * This displays a list of steps, marking them as complete or upcoming as sort of a progress bar.
     *
     * @since 5.2.2
     */
    protected function render_steps() {

        ?>
        <ol class="wc-setup-steps">

            <?php foreach ( $this->steps as $id => $step ) : ?>

                <?php if ( $id === $this->current_step ) : ?>
                    <li class="active"><?php echo esc_html( $step['name'] ); ?></li>
                <?php elseif ( $this->is_step_complete( $id ) ) : ?>
                    <li class="done"><a href="<?php echo esc_url( $this->get_step_url( $id ) ); ?>"><?php echo esc_html( $step['name'] ); ?></a></li>
                <?php else : ?>
                    <li><?php echo esc_html( $step['name'] ); ?></li>
                <?php endif;?>

            <?php endforeach; ?>

            <li class="<?php echo $this->is_finished() ? 'done' : ''; ?>"><?php esc_html_e( 'Ready!', 'woocommerce-plugin-framework' ); ?></li>

        </ol>
        <?php
    }


    /** Content Methods ***********************************************************************************************/


    /**
     * Renders the setup content.
     *
     * This will display the welcome screen, finished screen, or a specific step's markup.
     *
     * @since 5.2.2
     *
     * @param string $error_message custom error message
     */
    protected function render_content( $error_message = '' ) {

        ?>
        <div class="wc-setup-content sv-wc-plugin-admin-setup-content <?php echo esc_attr( $this->get_slug() ) . '-content'; ?>">

            <?php if ( $this->is_finished() ) : ?>

                <?php $this->render_finished(); ?>

                <?php $this->complete_setup(); ?>

            <?php else : ?>

                <?php // render a welcome message if the current is the first step ?>
                <?php if ( $this->is_started() ) : ?>
                    <?php $this->render_welcome(); ?>
                <?php endif; ?>

                <?php // render any error message from a previous save ?>
                <?php if ( ! empty( $error_message ) ) : ?>
                    <?php $this->render_error( $error_message ); ?>
                <?php endif; ?>

                <form method="post">
                    <?php $this->render_step( $this->current_step ); ?>
                    <?php wp_nonce_field( "wc_{$this->id}_setup_wizard_save", 'nonce' ); ?>
                </form>

            <?php endif; ?>

        </div>
        <?php
    }


    /**
     * Renders a save error.
     *
     * @since 5.2.2
     *
     * @param string $message error message to render
     */
    protected function render_error( $message ) {

        if ( ! empty( $message ) ) {

            printf( '<p class="error">%s</p>', esc_html( $message ) );
        }
    }


    /**
     * Renders a default welcome note.
     *
     * @since 5.2.2
     */
    protected function render_welcome() {

        ?>
        <h1><?php $this->render_welcome_heading()?></h1>
        <p class="welcome"><?php $this->render_welcome_text(); ?></p>
        <?php
    }


    /**
     * Renders the default welcome note heading.
     *
     * @since 5.2.2
     */
    protected function render_welcome_heading() {

        printf(
            /* translators: Placeholder: %s - plugin name */
            esc_html__( 'Welcome to %s!', 'woocommerce-plugin-framework' ),
            $this->get_plugin()->get_plugin_name()
        );
    }


    /**
     * Renders the default welcome note text.
     *
     * @since 5.2.2
     */
    protected function render_welcome_text() {

        esc_html_e( 'This quick setup wizard will help you configure the basic settings and get you started.', 'woocommerce-plugin-framework' );
    }


    /**
     * Renders the finished screen markup.
     *
     * This is what gets displayed after all of the steps have been completed or skipped.
     *
     * @since 5.2.2
     */
    protected function render_finished() {

        ?>
        <h1>
            <?php

            printf(
                /* translators: Placeholder: %s - the plugin name */
                esc_html__( '%s is ready!', 'woocommerce-plugin-framework' ),
                esc_html( $this->get_plugin()->get_plugin_name()
            ) );

            ?>
        </h1>
        <?php $this->render_before_next_steps(); ?>
        <?php $this->render_next_steps(); ?>
        <?php $this->render_after_next_steps(); ?>
        <?php
    }


    /**
     * Renders HTML before the next steps in the finished step screen.
     *
     * Plugins can implement this method to output additional HTML before the next steps are printed.
     *
     * @since 5.2.2
     */
    protected function render_before_next_steps() {
        // stub method
    }


    /**
     * Renders HTML after the next steps in the finished step screen.
     *
     * Plugins can implement this method to output additional HTML after the next steps are printed.
     *
     * @since 5.2.2
     */
    protected function render_after_next_steps() {
        // stub method
    }


    /**
     * Renders the next steps.
     *
     * @since 5.2.2
     */
    protected function render_next_steps() {

        $next_steps         = $this->get_next_steps();
        $additional_actions = $this->get_additional_actions();

        if ( ! empty( $next_steps ) || ! empty( $additional_actions ) ) :

            ?>
            <ul class="wc-wizard-next-steps">

                <?php foreach ( $next_steps as $step ) : ?>

                    <li class="wc-wizard-next-step-item">
                        <div class="wc-wizard-next-step-description">

                            <p class="next-step-heading"><?php esc_html_e( 'Next step', 'woocommerce-plugin-framework' ); ?></p>
                            <h3 class="next-step-description"><?php echo esc_html( $step['label'] ); ?></h3>

                            <?php if ( ! empty( $step['description'] ) ) : ?>
                                <p class="next-step-extra-info"><?php echo esc_html( $step['description'] ); ?></p>
                            <?php endif; ?>

                        </div>

                        <div class="wc-wizard-next-step-action">
                            <p class="wc-setup-actions step">
                                <?php $button_class = isset( $step['button_class'] ) ? $step['button_class'] : 'button button-primary button-large'; ?>
                                <?php $button_class = is_string( $button_class ) || is_array( $button_class ) ? array_map( 'sanitize_html_class', explode( ' ', implode( ' ', (array) $button_class ) ) ) : ''; ?>
                                <a class="<?php echo implode( ' ', $button_class ); ?>" href="<?php echo esc_url( $step['url'] ); ?>">
                                    <?php echo esc_html( $step['name'] ); ?>
                                </a>
                            </p>
                        </div>
                    </li>

                <?php endforeach; ?>

                <?php if ( ! empty( $additional_actions ) ) : ?>

                    <li class="wc-wizard-additional-steps">
                        <div class="wc-wizard-next-step-description">
                            <p class="next-step-heading"><?php esc_html_e( 'You can also:', 'woocommerce-plugin-framework' ); ?></p>
                        </div>
                        <div class="wc-wizard-next-step-action">

                            <p class="wc-setup-actions step">

                                <?php foreach ( $additional_actions as $name => $url ) : ?>

                                    <a class="button button-large" href="<?php echo esc_url( $url ); ?>">
                                        <?php echo esc_html( $name ); ?>
                                    </a>

                                <?php endforeach; ?>

                            </p>
                        </div>
                    </li>

                <?php endif; ?>

            </ul>
            <?php

        endif;
    }


    /**
     * Gets the next steps.
     *
     * These are major actions a user can take after finishing the setup wizard.
     * For instance, things like "Create your first Add-On" could go here.
     *
     * @since 5.2.2
     *
     * @return array
     */
    protected function get_next_steps() {

        $steps = array();

        if ( $this->get_plugin()->get_documentation_url() ) {

            $steps['view-docs'] = array(
                'name'        => __( 'View the Docs', 'woocommerce-plugin-framework' ),
                'label'       => __( 'See more setup options', 'woocommerce-plugin-framework' ),
                'description' => __( 'Learn more about customizing the plugin', 'woocommerce-plugin-framework' ),
                'url'         => $this->get_plugin()->get_documentation_url(),
            );
        }

        return $steps;
    }


    /**
     * Gets the additional steps.
     *
     * These are secondary actions.
     *
     * @since 5.2.2
     *
     * @return array
     */
    protected function get_additional_actions() {

        $next_steps = $this->get_next_steps();
        $actions    = array();

        if ( $this->get_plugin()->get_settings_url() ) {
            $actions[ __( 'Review Your Settings', 'woocommerce-plugin-framework' ) ] = $this->get_plugin()->get_settings_url();
        }

        if ( empty( $next_steps['view-docs'] ) && $this->get_plugin()->get_documentation_url() ) {
            $actions[ __( 'View the Docs', 'woocommerce-plugin-framework' ) ] = $this->get_plugin()->get_documentation_url();
        }

        if ( $this->get_plugin()->get_reviews_url() ) {
            $actions[ __( 'Leave a Review', 'woocommerce-plugin-framework' ) ] = $this->get_plugin()->get_reviews_url();
        }

        return $actions;
    }


    /**
     * Renders a given step's markup.
     *
     * This will display a title, whatever get's rendered by the step's view
     * callback, then the navigation buttons.
     *
     * @since 5.2.2
     *
     * @param string $step_id step ID to render
     */
    protected function render_step( $step_id ) {

        call_user_func( $this->steps[ $step_id ]['view'], $this );

        ?>
        <p class="wc-setup-actions step">

            <?php $label = __( 'Continue', 'woocommerce-plugin-framework' ); ?>

            <?php if ( is_callable( $this->steps[ $step_id ]['save'] ) ) : ?>

                <button
                    type="submit"
                    name="save_step"
                    class="button-primary button button-large button-next"
                    value="<?php echo esc_attr( $label ); ?>">
                    <?php echo esc_html( $label ); ?>
                </button>

            <?php else : ?>

                <a class="button-primary button button-large button-next" href="<?php echo esc_url( $this->get_next_step_url( $step_id ) ); ?>"><?php echo esc_html( $label ); ?></a>

            <?php endif; ?>
        </p>
        <?php
    }


    /**
     * Renders a form field.
     *
     * Call this in the same way as woocommerce_form_field().
     *
     * @since 5.2.2
     *
     * @param string $key field key
     * @param array $args field args - @see woocommerce_form_field()
     * @param string|null $value field value
     */
    protected function render_form_field( $key, $args, $value = null ) {

        if ( ! isset( $args['class'] ) ) {
            $args['class'] = array();
        } else {
            $args['class'] = (array) $args['class'];
        }

        // the base wrapper class for our styling
        $args['class'][] = 'sv-wc-plugin-admin-setup-control';

        // add the "required" HTML attribute for browser form validation
        if ( ! empty( $args['required'] ) ) {
            $args['custom_attributes']['required'] = true;
        }

        // all dropdowns are treated as enhanced selects
        if ( isset( $args['type'] ) && 'select' === $args['type'] ) {
            $args['input_class'][] = 'wc-enhanced-select';
        }

        // always echo the field
        $args['return'] = false;

        if ( isset( $args['type'] ) && 'toggle' === $args['type'] ) {
            $this->render_toggle_form_field( $key, $args, $value );
        } else {
            woocommerce_form_field( $key, $args, $value );
        }
    }


    /**
     * Renders the toggle form field.
     *
     * This requires special markup for the toggle UI.
     *
     * @since 5.2.2
     *
     * @param string $key field key
     * @param array $args field args - @see woocommerce_form_field()
     * @param string|null $value field value
     */
    public function render_toggle_form_field( $key, $args, $value ) {

        $args = wp_parse_args( $args, array(
            'type'              => 'text',
            'label'             => '',
            'description'       => '',
            'required'          => false,
            'id'                => $key,
            'class'             => array(),
            'label_class'       => array(),
            'input_class'       => array(),
            'custom_attributes' => array(),
            'default'           => false,
            'allow_html'        => false,
        ) );

        $args['class'][] = 'toggle';

        if ( $args['required'] ) {
            $args['class'][] = 'validate-required';
        }

        if ( null === $value ) {
            $value = $args['default'];
        }

        $custom_attributes         = array();
        $args['custom_attributes'] = array_filter( (array) $args['custom_attributes'], 'strlen' );

        if ( $args['description'] ) {
            $args['custom_attributes']['aria-describedby'] = $args['id'] . '-description';
        }

        if ( ! empty( $args['custom_attributes'] ) && is_array( $args['custom_attributes'] ) ) {
            foreach ( $args['custom_attributes'] as $attribute => $attribute_value ) {
                $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"';
            }
        }

        $enabled = $value || $args['default'];

        if ( $enabled ) {
            $args['class'][] = 'enabled';
        }

        ?>
        <div class="form-row <?php echo esc_attr( implode( ' ', $args['class'] ) ); ?>">

            <p class="name"><?php echo true === $args['allow_html'] ? $args['label'] : esc_html( $args['label'] ); ?></p>

            <?php if ( true === $args['allow_html'] ) : ?>
                <div class="content"><p class="description"><?php echo $args['description']; ?></p></div>
            <?php else : ?>
                <p class="content description"><?php echo esc_html( $args['description'] ); ?></p>
            <?php endif; ?>

            <div class="enable">
                <span class="toggle <?php echo $enabled ? '' : 'disabled'; ?>">
                    <input
                        id="<?php echo esc_attr( $args['id'] ); ?>"
                        type="checkbox"
                        class="input-checkbox <?php echo esc_attr( implode( ' ', $args['input_class'] ) ); ?>"
                        name="<?php echo esc_attr( $key ); ?>"
                        value="yes" <?php checked( true, $value ); ?>
                        <?php echo implode( ' ', $custom_attributes ); ?>
                    />
                    <label for="<?php echo esc_attr( $args['id'] ); ?>" class="<?php implode(  ' ', (array) $args['label_class'] ); ?>">
                </span>
            </div>

        </div>
        <?php
    }


    /**
     * Renders the setup footer.
     *
     * @since 5.2.2
     */
    protected function render_footer() {

        ?>
        <?php if ( $this->is_finished() ) : ?>
            <a class="wc-setup-footer-links" href="<?php echo esc_url( $this->get_dashboard_url() ); ?>"><?php esc_html_e( 'Return to the WordPress Dashboard', 'woocommerce-plugin-framework' ); ?></a>
        <?php elseif ( $this->is_started() ) : ?>
            <a class="wc-setup-footer-links" href="<?php echo esc_url( $this->get_dashboard_url() ); ?>"><?php esc_html_e( 'Not right now', 'woocommerce-plugin-framework' ); ?></a>
        <?php else : ?>
            <a class="wc-setup-footer-links" href="<?php echo esc_url( $this->get_next_step_url() ); ?>"><?php esc_html_e( 'Skip this step', 'woocommerce-plugin-framework' ); ?></a>
        <?php endif; ?>
        <?php

        do_action( 'wp_print_footer_scripts' );
    }


    /** Helper Methods ************************************************************************************************/


    /**
     * Registers a step.
     *
     * @since 5.2.2
     *
     * @param string $id unique step ID
     * @param string $name step name for display
     * @param string|array $view_callback callback to render the step's content HTML
     * @param string|array|null $save_callback callback to save the step's form values
     * @return bool whether the step was successfully added
     */
    public function register_step( $id, $name, $view_callback, $save_callback = null ) {

        try {

            // invalid ID
            if ( ! is_string( $id ) || empty( $id ) || $this->has_step( $id ) ) {
                throw new Framework\SV_WC_Plugin_Exception( 'Invalid step ID' );
            }

            // invalid name
            if ( ! is_string( $name ) || empty( $name ) ) {
                throw new Framework\SV_WC_Plugin_Exception( 'Invalid step name' );
            }

            // invalid view callback
            if ( ! is_callable( $view_callback ) ) {
                throw new Framework\SV_WC_Plugin_Exception( 'Invalid view callback' );
            }

            // invalid save callback
            if ( null !== $save_callback && ! is_callable( $save_callback ) ) {
                throw new Framework\SV_WC_Plugin_Exception( 'Invalid save callback' );
            }

            $this->steps[ $id ] = array(
                'name' => $name,
                'view' => $view_callback,
                'save' => $save_callback,
            );

            return true;

        } catch ( Framework\SV_WC_Plugin_Exception $exception ) {

            wc_doing_it_wrong( __METHOD__, $exception->getMessage(), '5.2.2' );

            return false;
        }
    }


    /**
     * Marks the setup as complete.
     *
     * @since 5.2.2
     *
     * @return bool
     */
    public function complete_setup() {

        return update_option( "wc_{$this->id}_setup_wizard_complete", 'yes' );
    }


    /** Conditional Methods *******************************************************************************************/


    /**
     * Determines if the current page is the setup wizard page.
     *
     * @since 5.2.2
     *
     * @return bool
     */
    public function is_setup_page() {

        return is_admin() && $this->get_slug() === Framework\SV_WC_Helper::get_requested_value( 'page' );
    }


    /**
     * Determines if a step is the current one displayed.
     *
     * @since 5.2.2
     *
     * @param string $step_id step ID
     * @return bool
     */
    public function is_current_step( $step_id ) {

        return $this->current_step === $step_id;
    }


    /**
     * Determines if setup has started.
     *
     * @since 5.2.2
     *
     * @return bool
     */
    public function is_started() {

        $steps = array_keys( $this->steps );

        return $this->current_step && $this->current_step === reset( $steps );
    }


    /**
     * Determines if setup has completed all of the steps.
     *
     * @since 5.2.2
     *
     * @return bool
     */
    public function is_finished() {

        return self::ACTION_FINISH === Framework\SV_WC_Helper::get_requested_value( 'action' );
    }


    /**
     * Determines if the setup wizard has been completed.
     *
     * This will be true if any user has been redirected back to the regular
     * WordPress dashboard, either manually or after finishing the steps.
     *
     * @since 5.2.2
     *
     * @return bool
     */
    public function is_complete() {

        return 'yes' === get_option( "wc_{$this->id}_setup_wizard_complete", 'no' );
    }


    /**
     * Determines if the given step has been completed.
     *
     * @since 5.2.2
     *
     * @param string $step_id step ID to check
     * @return bool
     */
    public function is_step_complete( $step_id ) {

        return array_search( $this->current_step, array_keys( $this->steps ), true ) > array_search( $step_id, array_keys( $this->steps ), true ) || $this->is_finished();
    }


    /**
     * Determines if the wizard has steps to display.
     *
     * @since 5.2.2
     *
     * @return bool
     */
    public function has_steps() {

        return is_array( $this->steps ) && ! empty( $this->steps );
    }


    /**
     * Determines if this setup handler has a given step.
     *
     * @since 5.2.2
     *
     * @param string $step_id step ID to check
     * @return bool
     */
    public function has_step( $step_id ) {

        return ! empty( $this->steps[ $step_id ] );
    }


    /** Getter Methods ************************************************************************************************/


    /**
     * Gets a given step's title.
     *
     * @since 5.2.2
     *
     * @param string $step_id step ID (optional: will assume the current step if unspecified)
     * @return string
     */
    public function get_step_title( $step_id = '' ) {

        $step_title = '';

        if ( ! $step_id ) {
            $step_id = $this->current_step;
        }

        if ( isset( $this->steps[ $step_id ]['name'] ) ) {
            $step_title = $this->steps[ $step_id ]['name'];
        }

        return $step_title;
    }


    /**
     * Gets the Setup Wizard URL.
     *
     * @since 5.2.2
     *
     * @return string
     */
    public function get_setup_url() {

        return add_query_arg( 'page', $this->get_slug(), admin_url( 'index.php' ) );
    }


    /**
     * Gets the URL for the next step based on a current step.
     *
     * @since 5.2.2
     *
     * @param string $step_id step ID to base "next" off of - defaults to this class's internal pointer
     * @return string
     */
    public function get_next_step_url( $step_id = '' ) {

        if ( ! $step_id ) {
            $step_id = $this->current_step;
        }

        $steps = array_keys( $this->steps );

        // if on the last step, next is the final finish step
        if ( end( $steps ) === $step_id ) {

            $url = $this->get_finish_url();

        } else {

            $step_index = array_search( $step_id, $steps, true );

            // if the current step is found, use the next in the array. otherwise, the first
            $step = false !== $step_index ? $steps[ $step_index + 1 ] : reset( $steps );

            $url = add_query_arg( 'step', $step );
        }

        return $url;
    }


    /**
     * Gets a given step's URL.
     *
     * @since 5.2.2
     *
     * @param string $step_id step ID
     * @return string|false
     */
    public function get_step_url( $step_id ) {

        $url = false;

        if ( $this->has_step( $step_id ) ) {
            $url = add_query_arg( 'step', $step_id, remove_query_arg( 'action' ) );
        }

        return $url;
    }


    /**
     * Gets the "finish" action URL.
     *
     * @since 5.2.2
     *
     * @return string
     */
    protected function get_finish_url() {

        return add_query_arg( 'action', self::ACTION_FINISH, remove_query_arg( 'step' ) );
    }


    /**
     * Gets the return URL.
     *
     * Can be used to return the user to the dashboard. The plugin's settings URL
     * will be used if it exists, otherwise the general dashboard URL.
     *
     * @since 5.2.2
     *
     * @return string
     */
    protected function get_dashboard_url() {

        $settings_url  = $this->get_plugin()->get_settings_url();
        $dashboard_url = ! empty( $settings_url ) ? $settings_url : admin_url();

        return add_query_arg( "wc_{$this->id}_setup_wizard_complete", true, $dashboard_url );
    }


    /**
     * Gets the setup setup handler's slug.
     *
     * @since 5.2.2
     *
     * @return string
     */
    protected function get_slug() {

        return 'wc-' . $this->get_plugin()->get_id_dasherized() . '-setup';
    }


    /**
     * Gets the plugin instance.
     *
     * @since 5.2.2
     *
     * @return Framework\SV_WC_Plugin|Framework\SV_WC_Payment_Gateway_Plugin
     */
    protected function get_plugin() {

        return $this->plugin;
    }


}


endif;