HEX
Server: LiteSpeed
System: Linux eko108.isimtescil.net 4.18.0-477.21.1.lve.1.el8.x86_64 #1 SMP Tue Sep 5 23:08:35 UTC 2023 x86_64
User: uyarreklamcomtr (11202)
PHP: 7.4.33
Disabled: opcache_get_status
Upload Files
File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/Features.tar
AsyncProductEditorCategoryField/Init.php000064400000004641151542725270014426 0ustar00<?php
/**
 * WooCommerce Async Product Editor Category Field.
 */

namespace Automattic\WooCommerce\Admin\Features\AsyncProductEditorCategoryField;

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
use Automattic\WooCommerce\Admin\PageController;

/**
 * Loads assets related to the async category field for the product editor.
 */
class Init {

	const FEATURE_ID = 'async-product-editor-category-field';

	/**
	 * Constructor
	 */
	public function __construct() {
		if ( Features::is_enabled( self::FEATURE_ID ) ) {
			add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) );
			add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
			add_filter( 'woocommerce_taxonomy_args_product_cat', array( $this, 'add_metabox_args' ) );
		}
	}

	/**
	 * Adds meta_box_cb callback arguments for custom metabox.
	 *
	 * @param array $args Category taxonomy args.
	 * @return array $args category taxonomy args.
	 */
	public function add_metabox_args( $args ) {
		if ( ! isset( $args['meta_box_cb'] ) ) {
			$args['meta_box_cb']          = 'WC_Meta_Box_Product_Categories::output';
			$args['meta_box_sanitize_cb'] = 'taxonomy_meta_box_sanitize_cb_checkboxes';
		}
		return $args;
	}

	/**
	 * Enqueue scripts needed for the product form block editor.
	 */
	public function enqueue_scripts() {
		if ( ! PageController::is_embed_page() ) {
			return;
		}

		WCAdminAssets::register_script( 'wp-admin-scripts', 'product-category-metabox', true );
		wp_localize_script(
			'wc-admin-product-category-metabox',
			'wc_product_category_metabox_params',
			array(
				'search_categories_nonce'     => wp_create_nonce( 'search-categories' ),
				'search_taxonomy_terms_nonce' => wp_create_nonce( 'search-taxonomy-terms' ),
			)
		);
		wp_enqueue_script( 'product-category-metabox' );

	}

	/**
	 * Enqueue styles needed for the rich text editor.
	 */
	public function enqueue_styles() {
		if ( ! PageController::is_embed_page() ) {
			return;
		}
		$version = Constants::get_constant( 'WC_VERSION' );

		wp_register_style(
			'woocommerce_admin_product_category_metabox_styles',
			WCAdminAssets::get_url( 'product-category-metabox/style', 'css' ),
			array(),
			$version
		);
		wp_style_add_data( 'woocommerce_admin_product_category_metabox_styles', 'rtl', 'replace' );

		wp_enqueue_style( 'woocommerce_admin_product_category_metabox_styles' );
	}

}
Features.php000064400000027274151542725270007061 0ustar00<?php
/**
 * Features loader for features developed in WooCommerce Admin.
 */

namespace Automattic\WooCommerce\Admin\Features;

use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Internal\Admin\Loader;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;

/**
 * Features Class.
 */
class Features {
	/**
	 * Class instance.
	 *
	 * @var Loader instance
	 */
	protected static $instance = null;

	/**
	 * Optional features
	 *
	 * @var array
	 */
	protected static $optional_features = array(
		'navigation'                 => array( 'default' => 'no' ),
		'settings'                   => array( 'default' => 'no' ),
		'analytics'                  => array( 'default' => 'yes' ),
		'remote-inbox-notifications' => array( 'default' => 'yes' ),
	);

	/**
	 * Beta features
	 *
	 * @var array
	 */
	protected static $beta_features = array(
		'navigation',
		'new-product-management-experience',
		'settings',
	);

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->register_internal_class_aliases();
		// Load feature before WooCommerce update hooks.
		add_action( 'init', array( __CLASS__, 'load_features' ), 4 );
		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_load_beta_features_modal' ) );
		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'load_scripts' ), 15 );
		add_filter( 'admin_body_class', array( __CLASS__, 'add_admin_body_classes' ) );
		add_filter( 'update_option_woocommerce_allow_tracking', array( __CLASS__, 'maybe_disable_features' ), 10, 2 );
	}

	/**
	 * Gets a build configured array of enabled WooCommerce Admin features/sections, but does not respect optionally disabled features.
	 *
	 * @return array Enabled Woocommerce Admin features/sections.
	 */
	public static function get_features() {
		return apply_filters( 'woocommerce_admin_features', array() );
	}

	/**
	 * Gets the optional feature options as an associative array that can be toggled on or off.
	 *
	 * @return array
	 */
	public static function get_optional_feature_options() {
		$features = [];

		foreach ( array_keys( self::$optional_features ) as $optional_feature_key ) {
			$feature_class = self::get_feature_class( $optional_feature_key );

			if ( $feature_class ) {
				$features[ $optional_feature_key ] = $feature_class::TOGGLE_OPTION_NAME;
			}
		}
		return $features;
	}

	/**
	 * Returns if a specific wc-admin feature exists in the current environment.
	 *
	 * @param  string $feature Feature slug.
	 * @return bool Returns true if the feature exists.
	 */
	public static function exists( $feature ) {
		$features = self::get_features();
		return in_array( $feature, $features, true );
	}

	/**
	 * Get the feature class as a string.
	 *
	 * @param string $feature Feature name.
	 * @return string|null
	 */
	public static function get_feature_class( $feature ) {
		$feature       = str_replace( '-', '', ucwords( strtolower( $feature ), '-' ) );
		$feature_class = 'Automattic\\WooCommerce\\Admin\\Features\\' . $feature;

		if ( class_exists( $feature_class ) ) {
			return $feature_class;
		}

		// Handle features contained in subdirectory.
		if ( class_exists( $feature_class . '\\Init' ) ) {
			return $feature_class . '\\Init';
		}

		return null;
	}

	/**
	 * Class loader for enabled WooCommerce Admin features/sections.
	 */
	public static function load_features() {
		$features = self::get_features();
		foreach ( $features as $feature ) {
			$feature_class = self::get_feature_class( $feature );

			if ( $feature_class ) {
				new $feature_class();
			}
		}
	}

	/**
	 * Gets a build configured array of enabled WooCommerce Admin respecting optionally disabled features.
	 *
	 * @return array Enabled Woocommerce Admin features/sections.
	 */
	public static function get_available_features() {
		$features                      = self::get_features();
		$optional_feature_keys         = array_keys( self::$optional_features );
		$optional_features_unavailable = [];

		/**
		 * Filter allowing WooCommerce Admin optional features to be disabled.
		 *
		 * @param bool $disabled False.
		 */
		if ( apply_filters( 'woocommerce_admin_disabled', false ) ) {
			return array_values( array_diff( $features, $optional_feature_keys ) );
		}

		foreach ( $optional_feature_keys as $optional_feature_key ) {
			$feature_class = self::get_feature_class( $optional_feature_key );

			if ( $feature_class ) {
				$default = isset( self::$optional_features[ $optional_feature_key ]['default'] ) ?
					self::$optional_features[ $optional_feature_key ]['default'] :
					'no';

				// Check if the feature is currently being enabled, if it is continue.
				/* phpcs:disable WordPress.Security.NonceVerification */
				$feature_option = $feature_class::TOGGLE_OPTION_NAME;
				if ( isset( $_POST[ $feature_option ] ) && '1' === $_POST[ $feature_option ] ) {
					continue;
				}

				if ( 'yes' !== get_option( $feature_class::TOGGLE_OPTION_NAME, $default ) ) {
					$optional_features_unavailable[] = $optional_feature_key;
				}
			}
		}

		return array_values( array_diff( $features, $optional_features_unavailable ) );
	}

	/**
	 * Check if a feature is enabled.
	 *
	 * @param string $feature Feature slug.
	 * @return bool
	 */
	public static function is_enabled( $feature ) {
		$available_features = self::get_available_features();
		return in_array( $feature, $available_features, true );
	}

	/**
	 * Enable a toggleable optional feature.
	 *
	 * @param string $feature Feature name.
	 * @return bool
	 */
	public static function enable( $feature ) {
		$features = self::get_optional_feature_options();

		if ( isset( $features[ $feature ] ) ) {
			update_option( $features[ $feature ], 'yes' );
			return true;
		}

		return false;
	}

	/**
	 * Disable a toggleable optional feature.
	 *
	 * @param string $feature Feature name.
	 * @return bool
	 */
	public static function disable( $feature ) {
		$features = self::get_optional_feature_options();

		if ( isset( $features[ $feature ] ) ) {
			update_option( $features[ $feature ], 'no' );
			return true;
		}

		return false;
	}

	/**
	 * Disable features when opting out of tracking.
	 *
	 * @param string $old_value Old value.
	 * @param string $value New value.
	 */
	public static function maybe_disable_features( $old_value, $value ) {
		if ( 'yes' === $value ) {
			return;
		}

		foreach ( self::$beta_features as $feature ) {
			self::disable( $feature );
		}
	}

	/**
	 * Adds the Features section to the advanced tab of WooCommerce Settings
	 *
	 * @deprecated 7.0 The WooCommerce Admin features are now handled by the WooCommerce features engine (see the FeaturesController class).
	 *
	 * @param array $sections Sections.
	 * @return array
	 */
	public static function add_features_section( $sections ) {
		return $sections;
	}

	/**
	 * Adds the Features settings.
	 *
	 * @deprecated 7.0 The WooCommerce Admin features are now handled by the WooCommerce features engine (see the FeaturesController class).
	 *
	 * @param array  $settings Settings.
	 * @param string $current_section Current section slug.
	 * @return array
	 */
	public static function add_features_settings( $settings, $current_section ) {
		return $settings;
	}

	/**
	 * Conditionally loads the beta features tracking modal.
	 *
	 * @param string $hook Page hook.
	 */
	public static function maybe_load_beta_features_modal( $hook ) {
		if (
			'woocommerce_page_wc-settings' !== $hook ||
			! isset( $_GET['tab'] ) || 'advanced' !== $_GET['tab'] || // phpcs:ignore CSRF ok.
			! isset( $_GET['section'] ) || 'features' !== $_GET['section'] // phpcs:ignore CSRF ok.
		) {
			return;
		}
		$tracking_enabled = get_option( 'woocommerce_allow_tracking', 'no' );

		if ( empty( self::$beta_features ) ) {
			return;
		}

		if ( 'yes' === $tracking_enabled ) {
			return;
		}

		$rtl = is_rtl() ? '.rtl' : '';

		wp_enqueue_style(
			'wc-admin-beta-features-tracking-modal',
			WCAdminAssets::get_url( "beta-features-tracking-modal/style{$rtl}", 'css' ),
			array( 'wp-components' ),
			WCAdminAssets::get_file_version( 'css' )
		);

		wp_enqueue_script(
			'wc-admin-beta-features-tracking-modal',
			WCAdminAssets::get_url( 'wp-admin-scripts/beta-features-tracking-modal', 'js' ),
			array( 'wp-i18n', 'wp-element', WC_ADMIN_APP ),
			WCAdminAssets::get_file_version( 'js' ),
			true
		);
	}

	/**
	 * Loads the required scripts on the correct pages.
	 */
	public static function load_scripts() {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return;
		}

		$features         = self::get_features();
		$enabled_features = array();
		foreach ( $features as $key ) {
			$enabled_features[ $key ] = self::is_enabled( $key );
		}
		wp_add_inline_script( WC_ADMIN_APP, 'window.wcAdminFeatures = ' . wp_json_encode( $enabled_features ), 'before' );
	}


	/**
	 * Adds body classes to the main wp-admin wrapper, allowing us to better target elements in specific scenarios.
	 *
	 * @param string $admin_body_class Body class to add.
	 */
	public static function add_admin_body_classes( $admin_body_class = '' ) {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return $admin_body_class;
		}

		$classes = explode( ' ', trim( $admin_body_class ) );

		$features = self::get_features();
		foreach ( $features as $feature_key ) {
			$classes[] = sanitize_html_class( 'woocommerce-feature-enabled-' . $feature_key );
		}

		$admin_body_class = implode( ' ', array_unique( $classes ) );
		return " $admin_body_class ";
	}

	/**
	 * Alias internal features classes to make them backward compatible.
	 * We've moved our feature classes to src-internal as part of merging this
	 * repository with WooCommerce Core to form a monorepo.
	 * See https://wp.me/p90Yrv-2HY for details.
	 */
	private function register_internal_class_aliases() {
		$aliases = array(
			// new class => original class (this will be aliased).
			'Automattic\WooCommerce\Internal\Admin\WCPayPromotion\Init' => 'Automattic\WooCommerce\Admin\Features\WcPayPromotion\Init',
			'Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions\Init' => 'Automattic\WooCommerce\Admin\Features\RemoteFreeExtensions\Init',
			'Automattic\WooCommerce\Internal\Admin\ActivityPanels' => 'Automattic\WooCommerce\Admin\Features\ActivityPanels',
			'Automattic\WooCommerce\Internal\Admin\Analytics' => 'Automattic\WooCommerce\Admin\Features\Analytics',
			'Automattic\WooCommerce\Internal\Admin\Coupons' => 'Automattic\WooCommerce\Admin\Features\Coupons',
			'Automattic\WooCommerce\Internal\Admin\CouponsMovedTrait' => 'Automattic\WooCommerce\Admin\Features\CouponsMovedTrait',
			'Automattic\WooCommerce\Internal\Admin\CustomerEffortScoreTracks' => 'Automattic\WooCommerce\Admin\Features\CustomerEffortScoreTracks',
			'Automattic\WooCommerce\Internal\Admin\Homescreen' => 'Automattic\WooCommerce\Admin\Features\Homescreen',
			'Automattic\WooCommerce\Internal\Admin\Marketing' => 'Automattic\WooCommerce\Admin\Features\Marketing',
			'Automattic\WooCommerce\Internal\Admin\MobileAppBanner' => 'Automattic\WooCommerce\Admin\Features\MobileAppBanner',
			'Automattic\WooCommerce\Internal\Admin\RemoteInboxNotifications' => 'Automattic\WooCommerce\Admin\Features\RemoteInboxNotifications',
			'Automattic\WooCommerce\Internal\Admin\SettingsNavigationFeature' => 'Automattic\WooCommerce\Admin\Features\Settings',
			'Automattic\WooCommerce\Internal\Admin\ShippingLabelBanner' => 'Automattic\WooCommerce\Admin\Features\ShippingLabelBanner',
			'Automattic\WooCommerce\Internal\Admin\ShippingLabelBannerDisplayRules' => 'Automattic\WooCommerce\Admin\Features\ShippingLabelBannerDisplayRules',
			'Automattic\WooCommerce\Internal\Admin\WcPayWelcomePage' => 'Automattic\WooCommerce\Admin\Features\WcPayWelcomePage',
		);
		foreach ( $aliases as $new_class => $orig_class ) {
			class_alias( $new_class, $orig_class );
		}
	}
}
Navigation/CoreMenu.php000064400000030141151542725270011102 0ustar00<?php
/**
 * WooCommerce Navigation Core Menu
 *
 * @package Woocommerce Admin
 */

namespace Automattic\WooCommerce\Admin\Features\Navigation;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\Navigation\Menu;
use Automattic\WooCommerce\Admin\Features\Navigation\Screen;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;

/**
 * CoreMenu class. Handles registering Core menu items.
 */
class CoreMenu {
	/**
	 * Class instance.
	 *
	 * @var Menu instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	final public static function instance() {
		if ( ! static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Init.
	 */
	public function init() {
		add_action( 'admin_menu', array( $this, 'register_post_types' ) );
		// Add this after we've finished migrating menu items to avoid hiding these items.
		add_action( 'admin_menu', array( $this, 'add_dashboard_menu_items' ), PHP_INT_MAX );
	}

	/**
	 * Add registered admin settings as menu items.
	 */
	public static function get_setting_items() {
		// Let the Settings feature add pages to the navigation if enabled.
		if ( Features::is_enabled( 'settings' ) ) {
			return array();
		}

		// Calling this method adds pages to the below tabs filter on non-settings pages.
		\WC_Admin_Settings::get_settings_pages();
		$tabs = apply_filters( 'woocommerce_settings_tabs_array', array() );

		$menu_items = array();
		$order      = 0;
		foreach ( $tabs as $key => $setting ) {
			$order       += 10;
			$menu_items[] = (
			array(
				'parent'     => 'woocommerce-settings',
				'title'      => $setting,
				'capability' => 'manage_woocommerce',
				'id'         => 'settings-' . $key,
				'url'        => 'admin.php?page=wc-settings&tab=' . $key,
				'order'      => $order,
			)
			);
		}

		return $menu_items;
	}

	/**
	 * Get unfulfilled order count
	 *
	 * @return array
	 */
	public static function get_shop_order_count() {
		$status_counts = array_map( 'wc_orders_count', array( 'processing', 'on-hold' ) );
		return array_sum( $status_counts );
	}

	/**
	 * Get all menu categories.
	 *
	 * @return array
	 */
	public static function get_categories() {
		$analytics_enabled = Features::is_enabled( 'analytics' );
		return array(
			array(
				'title' => __( 'Orders', 'woocommerce' ),
				'id'    => 'woocommerce-orders',
				'badge' => self::get_shop_order_count(),
				'order' => 10,
			),
			array(
				'title' => __( 'Products', 'woocommerce' ),
				'id'    => 'woocommerce-products',
				'order' => 20,
			),
			$analytics_enabled ?
				array(
					'title' => __( 'Analytics', 'woocommerce' ),
					'id'    => 'woocommerce-analytics',
					'order' => 30,
				) : null,
			$analytics_enabled ?
				array(
					'title'  => __( 'Reports', 'woocommerce' ),
					'id'     => 'woocommerce-reports',
					'parent' => 'woocommerce-analytics',
					'order'  => 200,
				) : null,
			array(
				'title' => __( 'Marketing', 'woocommerce' ),
				'id'    => 'woocommerce-marketing',
				'order' => 40,
			),
			array(
				'title'  => __( 'Settings', 'woocommerce' ),
				'id'     => 'woocommerce-settings',
				'menuId' => 'secondary',
				'order'  => 20,
				'url'    => 'admin.php?page=wc-settings',
			),
			array(
				'title'  => __( 'Tools', 'woocommerce' ),
				'id'     => 'woocommerce-tools',
				'menuId' => 'secondary',
				'order'  => 30,
			),
		);
	}

	/**
	 * Get all menu items.
	 *
	 * @return array
	 */
	public static function get_items() {
		$order_items       = self::get_order_menu_items();
		$product_items     = Menu::get_post_type_items( 'product', array( 'parent' => 'woocommerce-products' ) );
		$product_tag_items = Menu::get_taxonomy_items(
			'product_tag',
			array(
				'parent' => 'woocommerce-products',
				'order'  => 30,
			)
		);
		$product_cat_items = Menu::get_taxonomy_items(
			'product_cat',
			array(
				'parent' => 'woocommerce-products',
				'order'  => 20,
			)
		);

		$coupon_items  = Menu::get_post_type_items( 'shop_coupon', array( 'parent' => 'woocommerce-marketing' ) );
		$setting_items = self::get_setting_items();
		$wca_items     = array();
		$wca_pages     = \Automattic\WooCommerce\Admin\PageController::get_instance()->get_pages();

		foreach ( $wca_pages as $page ) {
			if ( ! isset( $page['nav_args'] ) ) {
				continue;
			}

			$path = isset( $page['path'] ) ? $page['path'] : null;
			$item = array_merge(
				array(
					'id'         => $page['id'],
					'url'        => $path,
					'title'      => $page['title'][0],
					'capability' => isset( $page['capability'] ) ? $page['capability'] : 'manage_woocommerce',
				),
				$page['nav_args']
			);

			// Don't allow top-level items to be added to the primary menu.
			if ( ! isset( $item['parent'] ) || 'woocommerce' === $item['parent'] ) {
				$item['menuId'] = 'plugins';
			}

			$wca_items[] = $item;
		}

		$home_item = array();
		$setup_tasks_remaining = TaskLists::setup_tasks_remaining();
		if ( defined( '\Automattic\WooCommerce\Internal\Admin\Homescreen::MENU_SLUG' ) ) {
			$home_item = array(
				'id'              => 'woocommerce-home',
				'title'           => __( 'Home', 'woocommerce' ),
				'url'             => \Automattic\WooCommerce\Internal\Admin\Homescreen::MENU_SLUG,
				'order'           => 0,
				'matchExpression' => 'page=wc-admin((?!path=).)*$',
				'badge'           => $setup_tasks_remaining ? $setup_tasks_remaining : null,
			);
		}

		$customers_item = array();
		if ( Features::is_enabled( 'analytics' ) ) {
			$customers_item = array(
				'id'    => 'woocommerce-analytics-customers',
				'title' => __( 'Customers', 'woocommerce' ),
				'url'   => 'wc-admin&path=/customers',
				'order' => 50,
			);
		}

		$add_product_mvp = array();
		if ( Features::is_enabled( 'new-product-management-experience' ) ) {
			$add_product_mvp = array(
				'id'     => 'woocommerce-add-product-mbp',
				'title'  => __( 'Add New (MVP)', 'woocommerce' ),
				'url'    => 'admin.php?page=wc-admin&path=/add-product',
				'parent' => 'woocommerce-products',
				'order'  => 50,
			);
		}

		return array_merge(
			array(
				$home_item,
				$customers_item,
				$order_items['all'],
				$order_items['new'],
				$product_items['all'],
				$product_cat_items['default'],
				$product_tag_items['default'],
				array(
					'id'              => 'woocommerce-product-attributes',
					'title'           => __( 'Attributes', 'woocommerce' ),
					'url'             => 'edit.php?post_type=product&page=product_attributes',
					'capability'      => 'manage_product_terms',
					'order'           => 40,
					'parent'          => 'woocommerce-products',
					'matchExpression' => 'edit.php(?=.*[?|&]page=product_attributes(&|$|#))|edit-tags.php(?=.*[?|&]taxonomy=pa_)(?=.*[?|&]post_type=product(&|$|#))',
				),
				array_merge( $product_items['new'], array( 'order' => 50 ) ),
				$coupon_items['default'],
				// Marketplace category.
				array(
					'title'      => __( 'Marketplace', 'woocommerce' ),
					'capability' => 'manage_woocommerce',
					'id'         => 'woocommerce-marketplace',
					'url'        => 'wc-addons',
					'menuId'     => 'secondary',
					'order'      => 10,
				),
				$add_product_mvp,
			),
			// Tools category.
			self::get_tool_items(),
			// WooCommerce Admin items.
			$wca_items,
			// Settings category.
			$setting_items,
			// Legacy report items.
			self::get_legacy_report_items()
		);
	}

	/**
	 * Supplies menu items for orders.
	 *
	 * This varies depending on whether we are actively using traditional post type-based orders or the new custom
	 * table-based orders.
	 *
	 * @return ?array
	 */
	private static function get_order_menu_items(): ?array {
		if ( ! wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) {
			return Menu::get_post_type_items( 'shop_order', array( 'parent' => 'woocommerce-orders' ) );
		}

		$main_orders_menu = array(
			'title'      => __( 'Orders', 'woocommerce' ),
			'capability' => 'edit_others_shop_orders',
			'id'         => 'woocommerce-orders-default',
			'url'        => 'admin.php?page=wc-orders',
			'parent'     => 'woocommerce-orders',
		);

		$all_orders_entry          = $main_orders_menu;
		$all_orders_entry['id']    = 'woocommerce-orders-all-items';
		$all_orders_entry['order'] = 10;

		$new_orders_entry          = $main_orders_menu;
		$new_orders_entry['title'] = __( 'Add order', 'woocommerce' );
		$new_orders_entry['id']    = 'woocommerce-orders-add-item';
		$new_orders_entry['url']   = 'admin.php?page=TBD';
		$new_orders_entry['order'] = 20;

		return array(
			'default' => $main_orders_menu,
			'all'     => $all_orders_entry,
			'new'     => $new_orders_entry,
		);
	}

	/**
	 * Get items for tools category.
	 *
	 * @return array
	 */
	public static function get_tool_items() {
		$tabs = array(
			'status' => __( 'System status', 'woocommerce' ),
			'tools'  => __( 'Utilities', 'woocommerce' ),
			'logs'   => __( 'Logs', 'woocommerce' ),
		);
		$tabs = apply_filters( 'woocommerce_admin_status_tabs', $tabs );

		$order = 1;
		$items = array(
			array(
				'parent'     => 'woocommerce-tools',
				'title'      => __( 'Import / Export', 'woocommerce' ),
				'capability' => 'import',
				'id'         => 'tools-import-export',
				'url'        => 'import.php',
				'migrate'    => false,
				'order'      => 0,
			),
		);

		foreach ( $tabs as $key => $tab ) {
			$items[] = array(
				'parent'     => 'woocommerce-tools',
				'title'      => $tab,
				'capability' => 'manage_woocommerce',
				'id'         => 'tools-' . $key,
				'url'        => 'wc-status&tab=' . $key,
				'order'      => $order,
			);
			$order++;
		}

		return $items;
	}

	/**
	 * Get legacy report items.
	 *
	 * @return array
	 */
	public static function get_legacy_report_items() {
		$reports    = \WC_Admin_Reports::get_reports();
		$menu_items = array();

		$order = 0;
		foreach ( $reports as $key => $report ) {
			$menu_items[] = array(
				'parent'     => 'woocommerce-reports',
				'title'      => $report['title'],
				'capability' => 'view_woocommerce_reports',
				'id'         => $key,
				'url'        => 'wc-reports&tab=' . $key,
				'order'      => $order,
			);
			$order++;
		}

		return $menu_items;
	}

	/**
	 * Register all core post types.
	 */
	public function register_post_types() {
		Screen::register_post_type( 'shop_order' );
		Screen::register_post_type( 'product' );
		Screen::register_post_type( 'shop_coupon' );
	}

	/**
	 * Add the dashboard items to the WP menu to create a quick-access flyout menu.
	 */
	public function add_dashboard_menu_items() {
		global $submenu, $menu;
		$mapped_items = Menu::get_mapped_menu_items();
		$top_level    = $mapped_items['woocommerce'];

		// phpcs:disable
		if ( ! isset( $submenu['woocommerce'] ) || empty( $top_level ) ) {
			return;
		}

		$menuIds = array(
			'primary',
			'secondary',
			'favorites',
		);

		foreach ( $menuIds as $menuId ) {
			foreach( $top_level[ $menuId ] as $item ) {
				// Skip specific categories.
				if (
					in_array(
						$item['id'],
						array(
							'woocommerce-tools',
						),
						true
					)
				) {
					continue;
				}

				// Use the link from the first item if it's a category.
				if ( ! isset( $item['url'] ) ) {
					$categoryMenuId = $menuId === 'favorites' ? 'plugins' : $menuId;
					$category_items = $mapped_items[ $item['id'] ][ $categoryMenuId ];

					if ( ! empty( $category_items ) ) {
						$first_item = $category_items[0];


						$submenu['woocommerce'][] = array(
							$item['title'],
							$first_item['capability'],
							isset( $first_item['url'] ) ? $first_item['url'] : null,
							$item['title'],
						);
					}

					continue;
				}

				// Show top-level items.
				$submenu['woocommerce'][] = array(
					$item['title'],
					$item['capability'],
					isset( $item['url'] ) ? $item['url'] : null,
					$item['title'],
				);
			}
		}
		// phpcs:enable
	}

	/**
	 * Get items excluded from WooCommerce menu migration.
	 *
	 * @return array
	 */
	public static function get_excluded_items() {
		$excluded_items = array(
			'woocommerce',
			'wc-reports',
			'wc-settings',
			'wc-status',
		);

		return apply_filters( 'woocommerce_navigation_core_excluded_items', $excluded_items );
	}
}
Navigation/Favorites.php000064400000005171151542725270011334 0ustar00<?php
/**
 * WooCommerce Navigation Favorite
 *
 * @package Woocommerce Navigation
 */

namespace Automattic\WooCommerce\Admin\Features\Navigation;

use Automattic\WooCommerce\Internal\Admin\WCAdminUser;

/**
 * Contains logic for the WooCommerce Navigation menu.
 */
class Favorites {

	/**
	 * Array index of menu capability.
	 *
	 * @var int
	 */
	const META_NAME = 'navigation_favorites';

	/**
	 * Favorites instance.
	 *
	 * @var Favorites|null
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	final public static function instance() {
		if ( ! static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Set given favorites string to the user meta data.
	 *
	 * @param string|number $user_id User id.
	 * @param array         $favorites Array of favorite values to set.
	 */
	private static function set_meta_value( $user_id, $favorites ) {
		WCAdminUser::update_user_data_field( $user_id, self::META_NAME, wp_json_encode( (array) $favorites ) );
	}

	/**
	 * Add item to favorites
	 *
	 * @param string        $item_id Identifier of item to add.
	 * @param string|number $user_id Identifier of user to add to.
	 * @return WP_Error|Boolean   Throws exception if item already exists.
	 */
	public static function add_item( $item_id, $user_id ) {

		$all_favorites = self::get_all( $user_id );

		if ( in_array( $item_id, $all_favorites, true ) ) {
			return new \WP_Error(
				'woocommerce_favorites_already_exists',
				__( 'Favorite already exists', 'woocommerce' )
			);
		}

		$all_favorites[] = $item_id;

		self::set_meta_value( $user_id, $all_favorites );

		return true;
	}

	/**
	 * Remove item from favorites
	 *
	 * @param string        $item_id Identifier of item to remove.
	 * @param string|number $user_id Identifier of user to remove from.
	 * @return \WP_Error|Boolean   Throws exception if item does not exist.
	 */
	public static function remove_item( $item_id, $user_id ) {
		$all_favorites = self::get_all( $user_id );

		if ( ! in_array( $item_id, $all_favorites, true ) ) {
			return new \WP_Error(
				'woocommerce_favorites_does_not_exist',
				__( 'Favorite item not found', 'woocommerce' )
			);
		}

		$remaining = array_values( array_diff( $all_favorites, [ $item_id ] ) );

		self::set_meta_value( $user_id, $remaining );

		return true;
	}

	/**
	 * Get all registered favorites.
	 *
	 * @param string|number $user_id Identifier of user to query.
	 * @return WP_Error|Array
	 */
	public static function get_all( $user_id ) {
		$response = WCAdminUser::get_user_data_field( $user_id, self::META_NAME );

		return $response ? json_decode( $response, true ) : array();
	}

}
Navigation/Init.php000064400000007714151542725270010302 0ustar00<?php
/**
 * Navigation Experience
 *
 * @package Woocommerce Admin
 */

namespace Automattic\WooCommerce\Admin\Features\Navigation;

use Automattic\WooCommerce\Internal\Admin\Survey;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\Navigation\Screen;
use Automattic\WooCommerce\Admin\Features\Navigation\Menu;
use Automattic\WooCommerce\Admin\Features\Navigation\CoreMenu;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;

/**
 * Contains logic for the Navigation
 */
class Init {
	/**
	 * Option name used to toggle this feature.
	 */
	const TOGGLE_OPTION_NAME = 'woocommerce_navigation_enabled';

	/**
	 * Determines if the feature has been toggled on or off.
	 *
	 * @var boolean
	 */
	protected static $is_updated = false;

	/**
	 * Hook into WooCommerce.
	 */
	public function __construct() {
		add_action( 'update_option_' . self::TOGGLE_OPTION_NAME, array( $this, 'reload_page_on_toggle' ), 10, 2 );
		add_action( 'woocommerce_settings_saved', array( $this, 'maybe_reload_page' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'maybe_enqueue_opt_out_scripts' ) );

		if ( Features::is_enabled( 'navigation' ) ) {
			Menu::instance()->init();
			CoreMenu::instance()->init();
			Screen::instance()->init();
		}
	}

	/**
	 * Add the feature toggle to the features settings.
	 *
	 * @deprecated 7.0 The WooCommerce Admin features are now handled by the WooCommerce features engine (see the FeaturesController class).
	 *
	 * @param array $features Feature sections.
	 * @return array
	 */
	public static function add_feature_toggle( $features ) {
		return $features;
	}

	/**
	 * Determine if sufficient versions are present to support Navigation feature
	 */
	public function is_nav_compatible() {
		include_once ABSPATH . 'wp-admin/includes/plugin.php';

		$gutenberg_minimum_version = '9.0.0'; // https://github.com/WordPress/gutenberg/releases/tag/v9.0.0.
		$wp_minimum_version        = '5.6';
		$has_gutenberg             = is_plugin_active( 'gutenberg/gutenberg.php' );
		$gutenberg_version         = $has_gutenberg ? get_plugin_data( WP_PLUGIN_DIR . '/gutenberg/gutenberg.php' )['Version'] : false;

		if ( $gutenberg_version && version_compare( $gutenberg_version, $gutenberg_minimum_version, '>=' ) ) {
			return true;
		}

		// Get unmodified $wp_version.
		include ABSPATH . WPINC . '/version.php';

		// Strip '-src' from the version string. Messes up version_compare().
		$wp_version = str_replace( '-src', '', $wp_version );

		if ( version_compare( $wp_version, $wp_minimum_version, '>=' ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Reloads the page when the option is toggled to make sure all nav features are loaded.
	 *
	 * @param string $old_value Old value.
	 * @param string $value     New value.
	 */
	public static function reload_page_on_toggle( $old_value, $value ) {
		if ( $old_value === $value ) {
			return;
		}

		if ( 'yes' !== $value ) {
			update_option( 'woocommerce_navigation_show_opt_out', 'yes' );
		}

		self::$is_updated = true;
	}

	/**
	 * Reload the page if the setting has been updated.
	 */
	public static function maybe_reload_page() {
		if ( ! isset( $_SERVER['REQUEST_URI'] ) || ! self::$is_updated ) {
			return;
		}

		wp_safe_redirect( wp_unslash( $_SERVER['REQUEST_URI'] ) );
		exit();
	}

	/**
	 * Enqueue the opt out scripts.
	 */
	public function maybe_enqueue_opt_out_scripts() {
		if ( get_option( 'woocommerce_navigation_show_opt_out', 'no' ) !== 'yes' ) {
			return;
		}

		$rtl = is_rtl() ? '.rtl' : '';
		wp_enqueue_style(
			'wc-admin-navigation-opt-out',
			WCAdminAssets::get_url( "navigation-opt-out/style{$rtl}", 'css' ),
			array( 'wp-components' ),
			WCAdminAssets::get_file_version( 'css' )
		);

		WCAdminAssets::register_script( 'wp-admin-scripts', 'navigation-opt-out', true );
		wp_localize_script(
			'wc-admin-navigation-opt-out',
			'surveyData',
			array(
				'url' => Survey::get_url( '/new-navigation-opt-out' ),
			)
		);
		delete_option( 'woocommerce_navigation_show_opt_out' );
	}
}
Navigation/Menu.php000064400000054435151542725270010305 0ustar00<?php
/**
 * WooCommerce Navigation Menu
 *
 * @package Woocommerce Navigation
 */

namespace Automattic\WooCommerce\Admin\Features\Navigation;

use Automattic\WooCommerce\Admin\Features\Navigation\Favorites;
use Automattic\WooCommerce\Admin\Features\Navigation\Screen;
use Automattic\WooCommerce\Admin\Features\Navigation\CoreMenu;

/**
 * Contains logic for the WooCommerce Navigation menu.
 */
class Menu {
	/**
	 * Class instance.
	 *
	 * @var Menu instance
	 */
	protected static $instance = null;

	/**
	 * Array index of menu capability.
	 *
	 * @var int
	 */
	const CAPABILITY = 1;

	/**
	 * Array index of menu callback.
	 *
	 * @var int
	 */
	const CALLBACK = 2;

	/**
	 * Array index of menu callback.
	 *
	 * @var int
	 */
	const SLUG = 3;

	/**
	 * Array index of menu CSS class string.
	 *
	 * @var int
	 */
	const CSS_CLASSES = 4;

	/**
	 * Array of usable menu IDs.
	 */
	const MENU_IDS = array(
		'primary',
		'favorites',
		'plugins',
		'secondary',
	);

	/**
	 * Store menu items.
	 *
	 * @var array
	 */
	protected static $menu_items = array();

	/**
	 * Store categories with menu item IDs.
	 *
	 * @var array
	 */
	protected static $categories = array(
		'woocommerce' => array(),
	);

	/**
	 * Registered callbacks or URLs with migration boolean as key value pairs.
	 *
	 * @var array
	 */
	protected static $callbacks = array();

	/**
	 * Get class instance.
	 */
	final public static function instance() {
		if ( ! static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Init.
	 */
	public function init() {
		add_action( 'admin_menu', array( $this, 'add_core_items' ), 100 );
		add_filter( 'admin_enqueue_scripts', array( $this, 'enqueue_data' ), 20 );

		add_filter( 'admin_menu', array( $this, 'migrate_core_child_items' ), PHP_INT_MAX - 1 );
		add_filter( 'admin_menu', array( $this, 'migrate_menu_items' ), PHP_INT_MAX - 2 );
	}

	/**
	 * Convert a WordPress menu callback to a URL.
	 *
	 * @param string $callback Menu callback.
	 * @return string
	 */
	public static function get_callback_url( $callback ) {
		// Return the full URL.
		if ( strpos( $callback, 'http' ) === 0 ) {
			return $callback;
		}

		$pos  = strpos( $callback, '?' );
		$file = $pos > 0 ? substr( $callback, 0, $pos ) : $callback;
		if ( file_exists( ABSPATH . "/wp-admin/$file" ) ) {
			return $callback;
		}
		return 'admin.php?page=' . $callback;
	}

	/**
	 * Get the parent key if one exists.
	 *
	 * @param string $callback Callback or URL.
	 * @return string|null
	 */
	public static function get_parent_key( $callback ) {
		global $submenu;

		if ( ! $submenu ) {
			return null;
		}

		// This is already a parent item.
		if ( isset( $submenu[ $callback ] ) ) {
			return null;
		}

		foreach ( $submenu as $key => $menu ) {
			foreach ( $menu as $item ) {
				if ( $item[ self::CALLBACK ] === $callback ) {
					return $key;
				}
			}
		}

		return null;
	}

	/**
	 * Adds a top level menu item to the navigation.
	 *
	 * @param array $args Array containing the necessary arguments.
	 *    $args = array(
	 *      'id'      => (string) The unique ID of the menu item. Required.
	 *      'title'   => (string) Title of the menu item. Required.
	 *      'url'     => (string) URL or callback to be used. Required.
	 *      'order'   => (int) Menu item order.
	 *      'migrate' => (bool) Whether or not to hide the item in the wp admin menu.
	 *      'menuId'  => (string) The ID of the menu to add the category to.
	 *    ).
	 */
	private static function add_category( $args ) {
		if ( ! isset( $args['id'] ) || isset( self::$menu_items[ $args['id'] ] ) ) {
			return;
		}

		$defaults           = array(
			'id'         => '',
			'title'      => '',
			'order'      => 100,
			'migrate'    => true,
			'menuId'     => 'primary',
			'isCategory' => true,
		);
		$menu_item          = wp_parse_args( $args, $defaults );
		$menu_item['title'] = wp_strip_all_tags( wp_specialchars_decode( $menu_item['title'] ) );
		unset( $menu_item['url'] );
		unset( $menu_item['capability'] );

		if ( ! isset( $menu_item['parent'] ) ) {
			$menu_item['parent']          = 'woocommerce';
			$menu_item['backButtonLabel'] = __(
				'WooCommerce Home',
				'woocommerce'
			);
		}

		self::$menu_items[ $menu_item['id'] ]       = $menu_item;
		self::$categories[ $menu_item['id'] ]       = array();
		self::$categories[ $menu_item['parent'] ][] = $menu_item['id'];

		if ( isset( $args['url'] ) ) {
			self::$callbacks[ $args['url'] ] = $menu_item['migrate'];
		}
	}

	/**
	 * Adds a child menu item to the navigation.
	 *
	 * @param array $args Array containing the necessary arguments.
	 *    $args = array(
	 *      'id'              => (string) The unique ID of the menu item. Required.
	 *      'title'           => (string) Title of the menu item. Required.
	 *      'parent'          => (string) Parent menu item ID.
	 *      'capability'      => (string) Capability to view this menu item.
	 *      'url'             => (string) URL or callback to be used. Required.
	 *      'order'           => (int) Menu item order.
	 *      'migrate'         => (bool) Whether or not to hide the item in the wp admin menu.
	 *      'menuId'          => (string) The ID of the menu to add the item to.
	 *      'matchExpression' => (string) A regular expression used to identify if the menu item is active.
	 *    ).
	 */
	private static function add_item( $args ) {
		if ( ! isset( $args['id'] ) ) {
			return;
		}

		if ( isset( self::$menu_items[ $args['id'] ] ) ) {
			error_log(  // phpcs:ignore
				sprintf(
					/* translators: 1: Duplicate menu item path. */
					esc_html__( 'You have attempted to register a duplicate item with WooCommerce Navigation: %1$s', 'woocommerce' ),
					'`' . $args['id'] . '`'
				)
			);
			return;
		}

		$defaults           = array(
			'id'         => '',
			'title'      => '',
			'capability' => 'manage_woocommerce',
			'url'        => '',
			'order'      => 100,
			'migrate'    => true,
			'menuId'     => 'primary',
		);
		$menu_item          = wp_parse_args( $args, $defaults );
		$menu_item['title'] = wp_strip_all_tags( wp_specialchars_decode( $menu_item['title'] ) );
		$menu_item['url']   = self::get_callback_url( $menu_item['url'] );

		if ( ! isset( $menu_item['parent'] ) ) {
			$menu_item['parent'] = 'woocommerce';
		}

		$menu_item['menuId'] = self::get_item_menu_id( $menu_item );

		self::$menu_items[ $menu_item['id'] ]       = $menu_item;
		self::$categories[ $menu_item['parent'] ][] = $menu_item['id'];

		if ( isset( $args['url'] ) ) {
			self::$callbacks[ $args['url'] ] = $menu_item['migrate'];
		}
	}

	/**
	 * Get an item's menu ID from its parent.
	 *
	 * @param array $item Item args.
	 * @return string
	 */
	public static function get_item_menu_id( $item ) {
		$favorites = Favorites::get_all( get_current_user_id() );
		if ( is_array( $favorites ) && ! empty( $favorites ) && in_array( $item['id'], $favorites, true ) ) {
			return 'favorites';
		}

		if ( isset( $item['parent'] ) && isset( self::$menu_items[ $item['parent'] ] ) ) {
			$menu_id = self::$menu_items[ $item['parent'] ]['menuId'];
			return 'favorites' === $menu_id
				? 'plugins'
				: $menu_id;
		}

		return $item['menuId'];
	}

	/**
	 * Adds a plugin category.
	 *
	 * @param array $args Array containing the necessary arguments.
	 *    $args = array(
	 *      'id'      => (string) The unique ID of the menu item. Required.
	 *      'title'   => (string) Title of the menu item. Required.
	 *      'url'     => (string) URL or callback to be used. Required.
	 *      'migrate' => (bool) Whether or not to hide the item in the wp admin menu.
	 *      'order'   => (int) Menu item order.
	 *    ).
	 */
	public static function add_plugin_category( $args ) {
		$category_args = array_merge(
			$args,
			array(
				'menuId' => 'plugins',
			)
		);

		if ( ! isset( $category_args['parent'] ) ) {
			unset( $category_args['order'] );
		}

		$menu_id = self::get_item_menu_id( $category_args );
		if ( ! in_array( $menu_id, array( 'plugins', 'favorites' ), true ) ) {
			return;
		}

		$category_args['menuId'] = $menu_id;

		self::add_category( $category_args );
	}

	/**
	 * Adds a plugin item.
	 *
	 * @param array $args Array containing the necessary arguments.
	 *    $args = array(
	 *      'id'              => (string) The unique ID of the menu item. Required.
	 *      'title'           => (string) Title of the menu item. Required.
	 *      'parent'          => (string) Parent menu item ID.
	 *      'capability'      => (string) Capability to view this menu item.
	 *      'url'             => (string) URL or callback to be used. Required.
	 *      'migrate'         => (bool) Whether or not to hide the item in the wp admin menu.
	 *      'order'           => (int) Menu item order.
	 *      'matchExpression' => (string) A regular expression used to identify if the menu item is active.
	 *    ).
	 */
	public static function add_plugin_item( $args ) {
		if ( ! isset( $args['parent'] ) ) {
			unset( $args['order'] );
		}

		$item_args = array_merge(
			$args,
			array(
				'menuId' => 'plugins',
			)
		);

		$menu_id = self::get_item_menu_id( $item_args );

		if ( 'plugins' !== $menu_id ) {
			return;
		}

		self::add_item( $item_args );
	}

	/**
	 * Adds a plugin setting item.
	 *
	 * @param array $args Array containing the necessary arguments.
	 *    $args = array(
	 *      'id'         => (string) The unique ID of the menu item. Required.
	 *      'title'      => (string) Title of the menu item. Required.
	 *      'capability' => (string) Capability to view this menu item.
	 *      'url'        => (string) URL or callback to be used. Required.
	 *      'migrate'    => (bool) Whether or not to hide the item in the wp admin menu.
	 *    ).
	 */
	public static function add_setting_item( $args ) {
		unset( $args['order'] );

		if ( isset( $args['parent'] ) || isset( $args['menuId'] ) ) {
			error_log(  // phpcs:ignore
				sprintf(
					/* translators: 1: Duplicate menu item path. */
					esc_html__( 'The item ID %1$s attempted to register using an invalid option. The arguments `menuId` and `parent` are not allowed for add_setting_item()', 'woocommerce' ),
					'`' . $args['id'] . '`'
				)
			);
		}

		$item_args = array_merge(
			$args,
			array(
				'menuId' => 'secondary',
				'parent' => 'woocommerce-settings',
			)
		);

		self::add_item( $item_args );
	}



	/**
	 * Get menu item templates for a given post type.
	 *
	 * @param string $post_type Post type to add.
	 * @param array  $menu_args Arguments merged with the returned menu items.
	 * @return array
	 */
	public static function get_post_type_items( $post_type, $menu_args = array() ) {
		$post_type_object = get_post_type_object( $post_type );

		if ( ! $post_type_object || ! $post_type_object->show_in_menu ) {
			return;
		}

		$parent           = isset( $menu_args['parent'] ) ? $menu_args['parent'] . '-' : '';
		$match_expression = isset( $_GET['post'] ) && get_post_type( intval( $_GET['post'] ) ) === $post_type // phpcs:ignore WordPress.Security.NonceVerification
			? '(edit.php|post.php)'
			: null;

		return array(
			'default' => array_merge(
				array(
					'title'           => esc_attr( $post_type_object->labels->menu_name ),
					'capability'      => $post_type_object->cap->edit_posts,
					'id'              => $parent . $post_type,
					'url'             => "edit.php?post_type={$post_type}",
					'matchExpression' => $match_expression,
				),
				$menu_args
			),
			'all'     => array_merge(
				array(
					'title'           => esc_attr( $post_type_object->labels->all_items ),
					'capability'      => $post_type_object->cap->edit_posts,
					'id'              => "{$parent}{$post_type}-all-items",
					'url'             => "edit.php?post_type={$post_type}",
					'order'           => 10,
					'matchExpression' => $match_expression,
				),
				$menu_args
			),
			'new'     => array_merge(
				array(
					'title'      => esc_attr( $post_type_object->labels->add_new ),
					'capability' => $post_type_object->cap->create_posts,
					'id'         => "{$parent}{$post_type}-add-new",
					'url'        => "post-new.php?post_type={$post_type}",
					'order'      => 20,
				),
				$menu_args
			),
		);
	}

	/**
	 * Get menu item templates for a given taxonomy.
	 *
	 * @param string $taxonomy Taxonomy to add.
	 * @param array  $menu_args Arguments merged with the returned menu items.
	 * @return array
	 */
	public static function get_taxonomy_items( $taxonomy, $menu_args = array() ) {
		$taxonomy_object = get_taxonomy( $taxonomy );

		if ( ! $taxonomy_object || ! $taxonomy_object->show_in_menu ) {
			return;
		}

		$parent             = isset( $menu_args['parent'] ) ? $menu_args['parent'] . '-' : '';
		$product_type_query = ! empty( $taxonomy_object->object_type )
			? "&post_type={$taxonomy_object->object_type[0]}"
			: '';
		$match_expression   = 'term.php';                               // Match term.php pages.
		$match_expression  .= "(?=.*[?|&]taxonomy={$taxonomy}(&|$|#))"; // Lookahead to match a taxonomy URL param.
		$match_expression  .= '|';                                      // Or.
		$match_expression  .= 'edit-tags.php';                          // Match edit-tags.php pages.
		$match_expression  .= "(?=.*[?|&]taxonomy={$taxonomy}(&|$|#))"; // Lookahead to match a taxonomy URL param.

		return array(
			'default' => array_merge(
				array(
					'title'           => esc_attr( $taxonomy_object->labels->menu_name ),
					'capability'      => $taxonomy_object->cap->edit_terms,
					'id'              => $parent . $taxonomy,
					'url'             => "edit-tags.php?taxonomy={$taxonomy}{$product_type_query}",
					'matchExpression' => $match_expression,
				),
				$menu_args
			),
			'all'     => array_merge(
				array(
					'title'           => esc_attr( $taxonomy_object->labels->all_items ),
					'capability'      => $taxonomy_object->cap->edit_terms,
					'id'              => "{$parent}{$taxonomy}-all-items",
					'url'             => "edit-tags.php?taxonomy={$taxonomy}{$product_type_query}",
					'matchExpression' => $match_expression,
					'order'           => 10,
				),
				$menu_args
			),

		);
	}

	/**
	 * Add core menu items.
	 */
	public function add_core_items() {
		$categories = CoreMenu::get_categories();
		foreach ( $categories as $category ) {
			self::add_category( $category );
		}

		$items = CoreMenu::get_items();
		foreach ( $items as $item ) {
			if ( isset( $item['is_category'] ) && $item['is_category'] ) {
				self::add_category( $item );
			} else {
				self::add_item( $item );
			}
		}
	}

	/**
	 * Add an item or taxonomy.
	 *
	 * @param array $menu_item Menu item.
	 */
	public function add_item_and_taxonomy( $menu_item ) {
		if ( in_array( $menu_item[2], CoreMenu::get_excluded_items(), true ) ) {
			return;
		}

		$menu_item[2] = htmlspecialchars_decode( $menu_item[2] );

		// Don't add already added items.
		$callbacks = self::get_callbacks();
		if ( array_key_exists( $menu_item[2], $callbacks ) ) {
			return;
		}

		// Don't add these Product submenus because they are added elsewhere.
		if ( in_array( $menu_item[2], array( 'product_importer', 'product_exporter', 'product_attributes' ), true ) ) {
			return;
		}

		self::add_plugin_item(
			array(
				'title'      => $menu_item[0],
				'capability' => $menu_item[1],
				'id'         => sanitize_title( $menu_item[0] ),
				'url'        => $menu_item[2],
			)
		);

		// Determine if migrated items are a taxonomy or post_type. If they are, register them.
		$parsed_url   = wp_parse_url( $menu_item[2] );
		$query_string = isset( $parsed_url['query'] ) ? $parsed_url['query'] : false;

		if ( $query_string ) {
			$query = array();
			parse_str( $query_string, $query );

			if ( isset( $query['taxonomy'] ) ) {
				Screen::register_taxonomy( $query['taxonomy'] );
			} elseif ( isset( $query['post_type'] ) ) {
				Screen::register_post_type( $query['post_type'] );
			}
		}
	}

	/**
	 * Migrate any remaining WooCommerce child items.
	 *
	 * @param array $menu Menu items.
	 * @return array
	 */
	public function migrate_core_child_items( $menu ) {
		global $submenu;

		if ( ! isset( $submenu['woocommerce'] ) && ! isset( $submenu['edit.php?post_type=product'] ) ) {
			return $menu;
		}

		$main_items    = isset( $submenu['woocommerce'] ) ? $submenu['woocommerce'] : array();
		$product_items = isset( $submenu['edit.php?post_type=product'] ) ? $submenu['edit.php?post_type=product'] : array();

		foreach ( $main_items as $key => $menu_item ) {
			self::add_item_and_taxonomy( $menu_item );
			// phpcs:disable
			if ( ! isset( $menu_item[ self::CSS_CLASSES ] ) ) {
				$submenu['woocommerce'][ $key ][] .= ' hide-if-js';
			} else if ( strpos( $submenu['woocommerce'][ $key ][ self::CSS_CLASSES ], 'hide-if-js' ) !== false ) {
				continue;
			} else {
				$submenu['woocommerce'][ $key ][ self::CSS_CLASSES ] .= ' hide-if-js';
			}
			// phpcs:enable
		}

		foreach ( $product_items as $key => $menu_item ) {
			self::add_item_and_taxonomy( $menu_item );
		}

		return $menu;
	}

	/**
	 * Check if a menu item's callback is registered in the menu.
	 *
	 * @param array $menu_item Menu item args.
	 * @return bool
	 */
	public static function has_callback( $menu_item ) {
		if ( ! $menu_item || ! isset( $menu_item[ self::CALLBACK ] ) ) {
			return false;
		}

		$callback = $menu_item[ self::CALLBACK ];

		if (
			isset( self::$callbacks[ $callback ] ) &&
			self::$callbacks[ $callback ]
		) {
			return true;
		}

		if (
			isset( self::$callbacks[ self::get_callback_url( $callback ) ] ) &&
			self::$callbacks[ self::get_callback_url( $callback ) ]
		) {
			return true;
		}

		return false;
	}

	/**
	 * Hides all WP admin menus items and adds screen IDs to check for new items.
	 */
	public static function migrate_menu_items() {
		global $menu, $submenu;

		foreach ( $menu as $key => $menu_item ) {
			if ( self::has_callback( $menu_item ) ) {
				// Disable phpcs since we need to override submenu classes.
				// Note that `phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited` does not work to disable this check.
				// phpcs:disable
				$menu[ $key ][ self::CSS_CLASSES ] .= ' hide-if-js';
				// phps:enable
				continue;
			}

			// WordPress core menus make the parent item the same URL as the first child.
			$has_children = isset( $submenu[ $menu_item[ self::CALLBACK ] ] ) && isset( $submenu[ $menu_item[ self::CALLBACK ] ][0] );
			$first_child  = $has_children ? $submenu[ $menu_item[ self::CALLBACK ] ][0] : null;
			if ( 'woocommerce' !== $menu_item[2] && self::has_callback( $first_child ) ) {
				// Disable phpcs since we need to override submenu classes.
				// Note that `phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited` does not work to disable this check.
				// phpcs:disable
				$menu[ $key ][ self::CSS_CLASSES ] .= ' hide-if-js';
				// phps:enable
			}
		}

		// Remove excluded submenu items
		if ( isset( $submenu['woocommerce'] ) ) {
			foreach ( $submenu['woocommerce'] as $key => $submenu_item ) {
				if ( in_array( $submenu_item[ self::CALLBACK ], CoreMenu::get_excluded_items(), true ) ) {
					if ( isset( $submenu['woocommerce'][ $key ][ self::CSS_CLASSES ] ) ) {
						$submenu['woocommerce'][ $key ][ self::CSS_CLASSES ] .= ' hide-if-js';
					} else {
						$submenu['woocommerce'][ $key ][] = 'hide-if-js';
					}
				}
			}
		}

		foreach ( $submenu as $parent_key => $parent ) {
			foreach ( $parent as $key => $menu_item ) {
				if ( self::has_callback( $menu_item ) ) {
					// Disable phpcs since we need to override submenu classes.
					// Note that `phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited` does not work to disable this check.
					// phpcs:disable
					if ( ! isset( $menu_item[ self::SLUG ] ) ) {
						$submenu[ $parent_key ][ $key ][] = '';
					}
					if ( ! isset( $menu_item[ self::CSS_CLASSES ] ) ) {
						$submenu[ $parent_key ][ $key ][] .= ' hide-if-js';
					} else {
						$submenu[ $parent_key ][ $key ][ self::CSS_CLASSES ] .= ' hide-if-js';
					}
					// phps:enable
				}
			}
		}

		foreach ( array_keys( self::$callbacks ) as $callback ) {
			Screen::add_screen( $callback );
		}
	}

	/**
	 * Add a callback to identify and hide pages in the WP menu.
	 */
	public static function hide_wp_menu_item( $callback ) {
		self::$callbacks[ $callback ] = true;
	}

	/**
	 * Get registered menu items.
	 *
	 * @return array
	 */
	public static function get_items() {
		return apply_filters( 'woocommerce_navigation_menu_items', self::$menu_items );
	}

	/**
	 * Get registered menu items.
	 *
	 * @return array
	 */
	public static function get_category_items( $category ) {
		if ( ! isset( self::$categories[ $category ] ) ) {
			return array();
		}

		$menu_item_ids = self::$categories[ $category ];

		$category_menu_items = array();
		foreach ( $menu_item_ids as $id ) {
			if ( isset( self::$menu_items[ $id ] ) ) {
				$category_menu_items[] = self::$menu_items[ $id ];
			}
		}

		return apply_filters( 'woocommerce_navigation_menu_category_items', $category_menu_items );
	}

	/**
	 * Get registered callbacks.
	 *
	 * @return array
	 */
	public static function get_callbacks() {
		return apply_filters( 'woocommerce_navigation_callbacks', self::$callbacks );
	}

	/**
	 * Gets the menu item data mapped by category and menu ID.
	 *
	 * @return array
	 */
	public static function get_mapped_menu_items() {
		$menu_items   = self::get_items();
		$mapped_items = array();

		// Sort the items by order and title.
		$order     = array_column( $menu_items, 'order' );
		$title     = array_column( $menu_items, 'title' );
		array_multisort( $order, SORT_ASC, $title, SORT_ASC, $menu_items );

		foreach ( $menu_items as $id => $menu_item ) {
			$category_id = $menu_item[ 'parent' ];
			$menu_id     = $menu_item[ 'menuId' ];
			if ( ! isset( $mapped_items[ $category_id ] ) ) {
				$mapped_items[ $category_id ] = array();
				foreach ( self::MENU_IDS as $available_menu_id ) {
					$mapped_items[ $category_id ][ $available_menu_id ] = array();
				}
			}

			// Incorrect menu ID.
			if ( ! isset( $mapped_items[ $category_id ][ $menu_id ] ) ) {
				continue;
			}

			// Remove the item if the user cannot access it.
			if ( isset( $menu_item[ 'capability' ] ) && ! current_user_can( $menu_item[ 'capability' ] ) ) {
				continue;
			}

			$mapped_items[ $category_id ][ $menu_id ][] = $menu_item;
		}

		return $mapped_items;
	}

	/**
	 * Add the menu to the page output.
	 *
	 * @param array $menu Menu items.
	 * @return array
	 */
	public function enqueue_data( $menu ) {
		$data = array(
			'menuItems'     => array_values( self::get_items() ),
			'rootBackUrl'   => get_dashboard_url(),
		);

		wp_add_inline_script( WC_ADMIN_APP, 'window.wcNavigation = ' . wp_json_encode( $data ), 'before' );
	}
}
Navigation/Screen.php000064400000013106151542725270010606 0ustar00<?php
/**
 * WooCommerce Navigation Screen
 *
 * @package Woocommerce Navigation
 */

namespace Automattic\WooCommerce\Admin\Features\Navigation;

use Automattic\WooCommerce\Admin\Features\Navigation\Menu;

/**
 * Contains logic for the WooCommerce Navigation menu.
 */
class Screen {
	/**
	 * Class instance.
	 *
	 * @var Screen instance
	 */
	protected static $instance = null;

	/**
	 * Screen IDs of registered pages.
	 *
	 * @var array
	 */
	protected static $screen_ids = array();

	/**
	 * Registered post types.
	 *
	 * @var array
	 */
	protected static $post_types = array();

	/**
	 * Registered taxonomies.
	 *
	 * @var array
	 */
	protected static $taxonomies = array();

	/**
	 * Get class instance.
	 */
	final public static function instance() {
		if ( ! static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Init.
	 */
	public function init() {
		add_filter( 'admin_body_class', array( $this, 'add_body_class' ) );
	}

	/**
	 * Returns an array of filtered screen ids.
	 */
	public static function get_screen_ids() {
		return apply_filters( 'woocommerce_navigation_screen_ids', self::$screen_ids );
	}

	/**
	 * Returns an array of registered post types.
	 */
	public static function get_post_types() {
		return apply_filters( 'woocommerce_navigation_post_types', self::$post_types );
	}

	/**
	 * Returns an array of registered post types.
	 */
	public static function get_taxonomies() {
		return apply_filters( 'woocommerce_navigation_taxonomies', self::$taxonomies );
	}

	/**
	 * Check if we're on a WooCommerce page
	 *
	 * @return bool
	 */
	public static function is_woocommerce_page() {
		global $pagenow;

		// Get taxonomy if on a taxonomy screen.
		$taxonomy = '';
		if ( in_array( $pagenow, array( 'edit-tags.php', 'term.php' ), true ) ) {
			if ( isset( $_GET['taxonomy'] ) ) { // phpcs:ignore CSRF ok.
				$taxonomy = sanitize_text_field( wp_unslash( $_GET['taxonomy'] ) ); // phpcs:ignore CSRF ok.
			}
		}
		$taxonomies = self::get_taxonomies();

		// Get post type if on a post screen.
		$post_type = '';
		if ( in_array( $pagenow, array( 'edit.php', 'post.php', 'post-new.php' ), true ) ) {
			if ( isset( $_GET['post'] ) ) { // phpcs:ignore CSRF ok.
				$post_type = get_post_type( (int) $_GET['post'] ); // phpcs:ignore CSRF ok.
			} elseif ( isset( $_GET['post_type'] ) ) { // phpcs:ignore CSRF ok.
				$post_type = sanitize_text_field( wp_unslash( $_GET['post_type'] ) ); // phpcs:ignore CSRF ok.
			}
		}
		$post_types = self::get_post_types();

		// Get current screen ID.
		$current_screen    = get_current_screen();
		$screen_ids        = self::get_screen_ids();
		$current_screen_id = $current_screen ? $current_screen->id : null;

		if (
			in_array( $post_type, $post_types, true ) ||
			in_array( $taxonomy, $taxonomies, true ) ||
			self::is_woocommerce_core_taxonomy( $taxonomy ) ||
			in_array( $current_screen_id, $screen_ids, true )
		) {
			return true;
		}

		return false;
	}

	/**
	 * Check if a given taxonomy is a WooCommerce core related taxonomy.
	 *
	 * @param string $taxonomy Taxonomy.
	 * @return bool
	 */
	public static function is_woocommerce_core_taxonomy( $taxonomy ) {
		if ( in_array( $taxonomy, array( 'product_cat', 'product_tag' ), true ) ) {
			return true;
		}

		if ( 'pa_' === substr( $taxonomy, 0, 3 ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Add navigation classes to body.
	 *
	 * @param string $classes Classes.
	 * @return string
	 */
	public function add_body_class( $classes ) {
		if ( self::is_woocommerce_page() ) {
			$classes .= ' has-woocommerce-navigation';

			/**
			 * Adds the ability to skip disabling of the WP toolbar.
			 *
			 * @param boolean $bool WP Toolbar disabled.
			 */
			if ( apply_filters( 'woocommerce_navigation_wp_toolbar_disabled', true ) ) {
				$classes .= ' is-wp-toolbar-disabled';
			}
		}

		return $classes;
	}

	/**
	 * Adds a screen ID to the list of screens that use the navigtion.
	 * Finds the parent if none is given to grab the correct screen ID.
	 *
	 * @param string      $callback Callback or URL for page.
	 * @param string|null $parent   Parent screen ID.
	 */
	public static function add_screen( $callback, $parent = null ) {
		global $submenu;

		$plugin_page = self::get_plugin_page( $callback );

		if ( ! $parent ) {
			$parent = Menu::get_parent_key( $callback );
		}

		$screen_id = get_plugin_page_hookname( $plugin_page, $parent );

		// This screen has already been added.
		if ( in_array( $screen_id, self::$screen_ids, true ) ) {
			return;
		}

		self::$screen_ids[] = $screen_id;
	}

	/**
	 * Get the plugin page slug.
	 *
	 * @param string $callback Callback.
	 * @return string
	 */
	public static function get_plugin_page( $callback ) {
		$url   = Menu::get_callback_url( $callback );
		$parts = wp_parse_url( $url );

		if ( ! isset( $parts['query'] ) ) {
			return $callback;
		}

		parse_str( $parts['query'], $query );

		if ( ! isset( $query['page'] ) ) {
			return $callback;
		}

		$plugin_page = wp_unslash( $query['page'] );
		$plugin_page = plugin_basename( $plugin_page );
		return $plugin_page;
	}

	/**
	 * Register post type for use in WooCommerce Navigation screens.
	 *
	 * @param string $post_type Post type to add.
	 */
	public static function register_post_type( $post_type ) {
		if ( ! in_array( $post_type, self::$post_types, true ) ) {
			self::$post_types[] = $post_type;
		}
	}

	/**
	 * Register taxonomy for use in WooCommerce Navigation screens.
	 *
	 * @param string $taxonomy Taxonomy to add.
	 */
	public static function register_taxonomy( $taxonomy ) {
		if ( ! in_array( $taxonomy, self::$taxonomies, true ) ) {
			self::$taxonomies[] = $taxonomy;
		}
	}
}
NewProductManagementExperience.php000064400000005022151542725270013365 0ustar00<?php
/**
 * WooCommerce New Product Management Experience
 */

namespace Automattic\WooCommerce\Admin\Features;

use Automattic\WooCommerce\Admin\Features\TransientNotices;
use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Internal\Admin\Loader;
use WP_Block_Editor_Context;

/**
 * Loads assets related to the new product management experience page.
 */
class NewProductManagementExperience {

	/**
	 * Option name used to toggle this feature.
	 */
	const TOGGLE_OPTION_NAME = 'woocommerce_new_product_management_enabled';

	/**
	 * Constructor
	 */
	public function __construct() {
		$this->maybe_show_disabled_notice();
		if ( ! Features::is_enabled( 'new-product-management-experience' ) ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) );
		add_action( 'get_edit_post_link', array( $this, 'update_edit_product_link' ), 10, 2 );
	}

	/**
	 * Maybe show disabled notice.
	 */
	public function maybe_show_disabled_notice() {
		$new_product_experience_param = 'new-product-experience-disabled';
		if ( isset( $_GET[ $new_product_experience_param ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			TransientNotices::add(
				array(
					'user_id' => get_current_user_id(),
					'id'      => 'new-product-experience-disbled',
					'status'  => 'success',
					'content' => __( '🌟‎ ‎ Thanks for the feedback. We’ll put it to good use!', 'woocommerce' ),
				)
			);

			$url = isset( $_SERVER['REQUEST_URI'] ) ? wc_clean( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
			$url = remove_query_arg( 'new-product-experience-disabled', $url );
			wp_safe_redirect( $url );
			exit;
		}
	}

	/**
	 * Enqueue styles needed for the rich text editor.
	 */
	public function enqueue_styles() {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return;
		}
		wp_enqueue_style( 'wp-edit-blocks' );
		wp_enqueue_style( 'wp-format-library' );
		wp_enqueue_editor();
		/**
		 * Enqueue any block editor related assets.
		 *
		 * @since 7.1.0
		*/
		do_action( 'enqueue_block_editor_assets' );
	}

	/**
	 * Update the edit product links when the new experience is enabled.
	 *
	 * @param string $link    The edit link.
	 * @param int    $post_id Post ID.
	 * @return string
	 */
	public function update_edit_product_link( $link, $post_id ) {
		$product = wc_get_product( $post_id );

		if ( ! $product ) {
			return $link;
		}

		if ( $product->get_type() === 'simple' ) {
			return admin_url( 'admin.php?page=wc-admin&path=/product/' . $product->get_id() );
		}

		return $link;
	}

}
Onboarding.php000064400000006356151542725270007363 0ustar00<?php
/**
 * WooCommerce Onboarding
 */

namespace Automattic\WooCommerce\Admin\Features;

use Automattic\WooCommerce\Admin\DeprecatedClassFacade;

/**
 * Contains backend logic for the onboarding profile and checklist feature.
 *
 * @deprecated since 6.3.0, use WooCommerce\Internal\Admin\Onboarding.
 */
class Onboarding extends DeprecatedClassFacade {
	/**
	 * The name of the non-deprecated class that this facade covers.
	 *
	 * @var string
	 */
	protected static $facade_over_classname = 'Automattic\WooCommerce\Admin\Features\Onboarding';

	/**
	 * The version that this class was deprecated in.
	 *
	 * @var string
	 */
	protected static $deprecated_in_version = '6.3.0';

	/**
	 * Hook into WooCommerce.
	 */
	public function __construct() {
	}

	/**
	 * Get a list of allowed industries for the onboarding wizard.
	 *
	 * @deprecated 6.3.0
	 * @return array
	 */
	public static function get_allowed_industries() {
		wc_deprecated_function( 'get_allowed_industries', '6.3', '\Automattic\WooCommerce\Internal\Admin\OnboardingIndustries::get_allowed_industries()' );
		return \Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingIndustries::get_allowed_industries();
	}

	/**
	 * Get a list of allowed product types for the onboarding wizard.
	 *
	 * @deprecated 6.3.0
	 * @return array
	 */
	public static function get_allowed_product_types() {
		wc_deprecated_function( 'get_allowed_product_types', '6.3', '\Automattic\WooCommerce\Internal\Admin\OnboardingProducts::get_allowed_product_types()' );
		return \Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProducts::get_allowed_product_types();
	}

	/**
	 * Get a list of themes for the onboarding wizard.
	 *
	 * @deprecated 6.3.0
	 * @return array
	 */
	public static function get_themes() {
		wc_deprecated_function( 'get_themes', '6.3', '\Automattic\WooCommerce\Internal\Admin\OnboardingThemes::get_themes()' );
		return \Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingThemes::get_themes();
	}

	/**
	 * Get theme data used in onboarding theme browser.
	 *
	 * @deprecated 6.3.0
	 * @param WP_Theme $theme Theme to gather data from.
	 * @return array
	 */
	public static function get_theme_data( $theme ) {
		wc_deprecated_function( 'get_theme_data', '6.3', '\Automattic\WooCommerce\Internal\Admin\OnboardingThemes::get_theme_data()' );
		return \Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingThemes::get_theme_data();
	}

	/**
	 * Gets an array of themes that can be installed & activated via the onboarding wizard.
	 *
	 * @deprecated 6.3.0
	 * @return array
	 */
	public static function get_allowed_themes() {
		wc_deprecated_function( 'get_allowed_themes', '6.3', '\Automattic\WooCommerce\Internal\Admin\OnboardingThemes::get_allowed_themes()' );
		return \Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingThemes::get_allowed_themes();
	}

	/**
	 * Get dynamic product data from API.
	 *
	 * @deprecated 6.3.0
	 * @param array $product_types Array of product types.
	 * @return array
	 */
	public static function get_product_data( $product_types ) {
		wc_deprecated_function( 'get_product_data', '6.3', '\Automattic\WooCommerce\Internal\Admin\OnboardingProducts::get_product_data()' );
		return \Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProducts::get_product_data();
	}
}
OnboardingTasks/DeprecatedExtendedTask.php000064400000006322151542725270014726 0ustar00<?php
/**
 * A temporary class for creating tasks on the fly from deprecated tasks.
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

/**
 * DeprecatedExtendedTask class.
 */
class DeprecatedExtendedTask extends Task {
	/**
	 * ID.
	 *
	 * @var string
	 */
	public $id = '';

	/**
	 * Additional info.
	 *
	 * @var string|null
	 */
	public $additional_info = '';

	/**
	 * Content.
	 *
	 * @var string
	 */
	public $content = '';

	/**
	 * Whether the task is complete or not.
	 *
	 * @var boolean
	 */
	public $is_complete = false;

	/**
	 * Snoozeable.
	 *
	 * @var boolean
	 */
	public $is_snoozeable = false;

	/**
	 * Dismissable.
	 *
	 * @var boolean
	 */
	public $is_dismissable = false;

	/**
	 * Whether the store is capable of viewing the task.
	 *
	 * @var bool
	 */
	public $can_view = true;

	/**
	 * Level.
	 *
	 * @var int
	 */
	public $level = 3;

	/**
	 * Time.
	 *
	 * @var string|null
	 */
	public $time;

	/**
	 * Title.
	 *
	 * @var string
	 */
	public $title = '';


	/**
	 * Constructor.
	 *
	 * @param TaskList $task_list Parent task list.
	 * @param array    $args Array of task args.
	 */
	public function __construct( $task_list, $args ) {
		parent::__construct( $task_list );
		$task_args = wp_parse_args(
			$args,
			array(
				'id'              => null,
				'is_dismissable'  => false,
				'is_snoozeable'   => false,
				'can_view'        => true,
				'level'           => 3,
				'additional_info' => null,
				'content'         => '',
				'title'           => '',
				'is_complete'     => false,
				'time'            => null,
			)
		);

		$this->id              = $task_args['id'];
		$this->additional_info = $task_args['additional_info'];
		$this->content         = $task_args['content'];
		$this->is_complete     = $task_args['is_complete'];
		$this->is_dismissable  = $task_args['is_dismissable'];
		$this->is_snoozeable   = $task_args['is_snoozeable'];
		$this->can_view        = $task_args['can_view'];
		$this->level           = $task_args['level'];
		$this->time            = $task_args['time'];
		$this->title           = $task_args['title'];
	}

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return $this->id;
	}

	/**
	 * Additional info.
	 *
	 * @return string
	 */
	public function get_additional_info() {
		return $this->additional_info;
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return $this->content;
	}

	/**
	 * Level.
	 *
	 * @return int
	 */
	public function get_level() {
		return $this->level;
	}

	/**
	 * Title
	 *
	 * @return string
	 */
	public function get_title() {
		return $this->title;
	}

	/**
	 * Time
	 *
	 * @return string|null
	 */
	public function get_time() {
		return $this->time;
	}

	/**
	 * Check if a task is snoozeable.
	 *
	 * @return bool
	 */
	public function is_snoozeable() {
		return $this->is_snoozeable;
	}

	/**
	 * Check if a task is dismissable.
	 *
	 * @return bool
	 */
	public function is_dismissable() {
		return $this->is_dismissable;
	}

	/**
	 * Check if a task is dismissable.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return $this->is_complete;
	}

	/**
	 * Check if a task is dismissable.
	 *
	 * @return bool
	 */
	public function can_view() {
		return $this->can_view;
	}
}
OnboardingTasks/DeprecatedOptions.php000064400000005000151542725270013766 0ustar00<?php
/**
 * Filters for maintaining backwards compatibility with deprecated options.
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\TaskList;
use WC_Install;

/**
 * DeprecatedOptions class.
 */
class DeprecatedOptions {
	/**
	 * Initialize.
	 */
	public static function init() {
		add_filter( 'pre_option_woocommerce_task_list_hidden', array( __CLASS__, 'get_deprecated_options' ), 10, 2 );
		add_filter( 'pre_option_woocommerce_extended_task_list_hidden', array( __CLASS__, 'get_deprecated_options' ), 10, 2 );
		add_action( 'pre_update_option_woocommerce_task_list_hidden', array( __CLASS__, 'update_deprecated_options' ), 10, 3 );
		add_action( 'pre_update_option_woocommerce_extended_task_list_hidden', array( __CLASS__, 'update_deprecated_options' ), 10, 3 );
	}

	/**
	 * Get the values from the correct source when attempting to retrieve deprecated options.
	 *
	 * @param string $pre_option Pre option value.
	 * @param string $option Option name.
	 * @return string
	 */
	public static function get_deprecated_options( $pre_option, $option ) {
		if ( defined( 'WC_INSTALLING' ) && WC_INSTALLING === true ) {
			return $pre_option;
		}

		$hidden = get_option( 'woocommerce_task_list_hidden_lists', array() );
		switch ( $option ) {
			case 'woocommerce_task_list_hidden':
				return in_array( 'setup', $hidden, true ) ? 'yes' : 'no';
			case 'woocommerce_extended_task_list_hidden':
				return in_array( 'extended', $hidden, true ) ? 'yes' : 'no';
		}
	}

	/**
	 * Updates the new option names when deprecated options are updated.
	 * This is a temporary fallback until we can fully remove the old task list components.
	 *
	 * @param string $value New value.
	 * @param string $old_value Old value.
	 * @param string $option Option name.
	 * @return string
	 */
	public static function update_deprecated_options( $value, $old_value, $option ) {
		switch ( $option ) {
			case 'woocommerce_task_list_hidden':
				$task_list = TaskLists::get_list( 'setup' );
				if ( ! $task_list ) {
					return;
				}
				$update = 'yes' === $value ? $task_list->hide() : $task_list->unhide();
				delete_option( 'woocommerce_task_list_hidden' );
				return false;
			case 'woocommerce_extended_task_list_hidden':
				$task_list = TaskLists::get_list( 'extended' );
				if ( ! $task_list ) {
					return;
				}
				$update = 'yes' === $value ? $task_list->hide() : $task_list->unhide();
				delete_option( 'woocommerce_extended_task_list_hidden' );
				return false;
		}
	}
}
OnboardingTasks/Init.php000064400000002116151542725270011262 0ustar00<?php
/**
 * WooCommerce Onboarding Tasks
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\DeprecatedOptions;

/**
 * Contains the logic for completing onboarding tasks.
 */
class Init {
	/**
	 * Class instance.
	 *
	 * @var OnboardingTasks instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor
	 */
	public function __construct() {
		DeprecatedOptions::init();
		TaskLists::init();
	}

	/**
	 * Get task item data for settings filter.
	 *
	 * @return array
	 */
	public static function get_settings() {
		$settings            = array();
		$wc_pay_is_connected = false;
		if ( class_exists( '\WC_Payments' ) ) {
			$wc_payments_gateway = \WC_Payments::get_gateway();
			$wc_pay_is_connected = method_exists( $wc_payments_gateway, 'is_connected' )
				? $wc_payments_gateway->is_connected()
				: false;
		}

		return $settings;
	}
}
OnboardingTasks/Task.php000064400000030111151542725270011255 0ustar00<?php
/**
 * Handles task related methods.
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

use Automattic\WooCommerce\Internal\Admin\WCAdminUser;

/**
 * Task class.
 */
abstract class Task {
	/**
	 * Task traits.
	 */
	use TaskTraits;

	/**
	 * Name of the dismiss option.
	 *
	 * @var string
	 */
	const DISMISSED_OPTION = 'woocommerce_task_list_dismissed_tasks';

	/**
	 * Name of the snooze option.
	 *
	 * @var string
	 *
	 * @deprecated 7.2.0
	 */
	const SNOOZED_OPTION = 'woocommerce_task_list_remind_me_later_tasks';

	/**
	 * Name of the actioned option.
	 *
	 * @var string
	 */
	const ACTIONED_OPTION = 'woocommerce_task_list_tracked_completed_actions';

	/**
	 * Option name of completed tasks.
	 *
	 * @var string
	 */
	const COMPLETED_OPTION = 'woocommerce_task_list_tracked_completed_tasks';

	/**
	 * Name of the active task transient.
	 *
	 * @var string
	 */
	const ACTIVE_TASK_TRANSIENT = 'wc_onboarding_active_task';

	/**
	 * Parent task list.
	 *
	 * @var TaskList
	 */
	protected $task_list;

	/**
	 * Duration to milisecond mapping.
	 *
	 * @var string
	 */
	protected $duration_to_ms = array(
		'day'  => DAY_IN_SECONDS * 1000,
		'hour' => HOUR_IN_SECONDS * 1000,
		'week' => WEEK_IN_SECONDS * 1000,
	);

	/**
	 * Constructor
	 *
	 * @param TaskList|null $task_list Parent task list.
	 */
	public function __construct( $task_list = null ) {
		$this->task_list = $task_list;
	}

	/**
	 * ID.
	 *
	 * @return string
	 */
	abstract public function get_id();

	/**
	 * Title.
	 *
	 * @return string
	 */
	abstract public function get_title();

	/**
	 * Content.
	 *
	 * @return string
	 */
	abstract public function get_content();

	/**
	 * Time.
	 *
	 * @return string
	 */
	abstract public function get_time();

	/**
	 * Parent ID.
	 *
	 * @return string
	 */
	public function get_parent_id() {
		if ( ! $this->task_list ) {
			return '';
		}
		return $this->task_list->get_list_id();
	}

	/**
	 * Get task list options.
	 *
	 * @return array
	 */
	public function get_parent_options() {
		if ( ! $this->task_list ) {
			return array();
		}
		return $this->task_list->options;
	}

	/**
	 * Get custom option.
	 *
	 * @param string $option_name name of custom option.
	 * @return mixed|null
	 */
	public function get_parent_option( $option_name ) {
		if ( $this->task_list && isset( $this->task_list->options[ $option_name ] ) ) {
			return $this->task_list->options[ $option_name ];
		}
		return null;
	}


	/**
	 * Prefix event for track event naming.
	 *
	 * @param string $event_name Event name.
	 * @return string
	 */
	public function prefix_event( $event_name ) {
		if ( ! $this->task_list ) {
			return '';
		}
		return $this->task_list->prefix_event( $event_name );
	}

	/**
	 * Additional info.
	 *
	 * @return string
	 */
	public function get_additional_info() {
		return '';
	}

	/**
	 * Additional data.
	 *
	 * @return mixed
	 */
	public function get_additional_data() {
		return null;
	}

	/**
	 * Badge.
	 *
	 * @return string
	 */
	public function get_badge() {
		return '';
	}

	/**
	 * Level.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return string
	 */
	public function get_level() {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		return 3;
	}

	/**
	 * Action label.
	 *
	 * @return string
	 */
	public function get_action_label() {
		return __( "Let's go", 'woocommerce' );
	}

	/**
	 * Action URL.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return null;
	}

	/**
	 * Check if a task is dismissable.
	 *
	 * @return bool
	 */
	public function is_dismissable() {
		return false;
	}

	/**
	 * Bool for task dismissal.
	 *
	 * @return bool
	 */
	public function is_dismissed() {
		if ( ! $this->is_dismissable() ) {
			return false;
		}

		$dismissed = get_option( self::DISMISSED_OPTION, array() );

		return in_array( $this->get_id(), $dismissed, true );
	}

	/**
	 * Dismiss the task.
	 *
	 * @return bool
	 */
	public function dismiss() {
		if ( ! $this->is_dismissable() ) {
			return false;
		}

		$dismissed   = get_option( self::DISMISSED_OPTION, array() );
		$dismissed[] = $this->get_id();
		$update      = update_option( self::DISMISSED_OPTION, array_unique( $dismissed ) );

		if ( $update ) {
			$this->record_tracks_event( 'dismiss_task', array( 'task_name' => $this->get_id() ) );
		}

		return $update;
	}

	/**
	 * Undo task dismissal.
	 *
	 * @return bool
	 */
	public function undo_dismiss() {
		$dismissed = get_option( self::DISMISSED_OPTION, array() );
		$dismissed = array_diff( $dismissed, array( $this->get_id() ) );
		$update    = update_option( self::DISMISSED_OPTION, $dismissed );

		if ( $update ) {
			$this->record_tracks_event( 'undo_dismiss_task', array( 'task_name' => $this->get_id() ) );
		}

		return $update;
	}

	/**
	 * Check if a task is snoozeable.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return bool
	 */
	public function is_snoozeable() {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		return false;
	}

	/**
	 * Get the snoozed until datetime.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return string
	 */
	public function get_snoozed_until() {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		$snoozed_tasks = get_option( self::SNOOZED_OPTION, array() );
		if ( isset( $snoozed_tasks[ $this->get_id() ] ) ) {
			return $snoozed_tasks[ $this->get_id() ];
		}

		return null;
	}

	/**
	 * Bool for task snoozed.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return bool
	 */
	public function is_snoozed() {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		if ( ! $this->is_snoozeable() ) {
			return false;
		}

		$snoozed = get_option( self::SNOOZED_OPTION, array() );

		return isset( $snoozed[ $this->get_id() ] ) && $snoozed[ $this->get_id() ] > ( time() * 1000 );
	}

	/**
	 * Snooze the task.
	 *
	 * @param string $duration Duration to snooze. day|hour|week.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return bool
	 */
	public function snooze( $duration = 'day' ) {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		if ( ! $this->is_snoozeable() ) {
			return false;
		}

		$snoozed                    = get_option( self::SNOOZED_OPTION, array() );
		$snoozed_until              = $this->duration_to_ms[ $duration ] + ( time() * 1000 );
		$snoozed[ $this->get_id() ] = $snoozed_until;
		$update                     = update_option( self::SNOOZED_OPTION, $snoozed );

		if ( $update ) {
			if ( $update ) {
				$this->record_tracks_event( 'remindmelater_task', array( 'task_name' => $this->get_id() ) );
			}
		}

		return $update;
	}

	/**
	 * Undo task snooze.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return bool
	 */
	public function undo_snooze() {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		$snoozed = get_option( self::SNOOZED_OPTION, array() );
		unset( $snoozed[ $this->get_id() ] );
		$update = update_option( self::SNOOZED_OPTION, $snoozed );

		if ( $update ) {
			$this->record_tracks_event( 'undo_remindmelater_task', array( 'task_name' => $this->get_id() ) );
		}

		return $update;
	}

	/**
	 * Check if a task list has previously been marked as complete.
	 *
	 * @return bool
	 */
	public function has_previously_completed() {
		$complete = get_option( self::COMPLETED_OPTION, array() );
		return in_array( $this->get_id(), $complete, true );
	}

	/**
	 * Track task completion if task is viewable.
	 */
	public function possibly_track_completion() {
		if ( ! $this->is_complete() ) {
			return;
		}

		if ( $this->has_previously_completed() ) {
			return;
		}

		$completed_tasks   = get_option( self::COMPLETED_OPTION, array() );
		$completed_tasks[] = $this->get_id();
		update_option( self::COMPLETED_OPTION, $completed_tasks );
		$this->record_tracks_event( 'task_completed', array( 'task_name' => $this->get_id() ) );
	}

	/**
	 * Set this as the active task across page loads.
	 */
	public function set_active() {
		if ( $this->is_complete() ) {
			return;
		}

		set_transient(
			self::ACTIVE_TASK_TRANSIENT,
			$this->get_id(),
			DAY_IN_SECONDS
		);
	}

	/**
	 * Check if this is the active task.
	 */
	public function is_active() {
		return get_transient( self::ACTIVE_TASK_TRANSIENT ) === $this->get_id();
	}

	/**
	 * Check if the store is capable of viewing the task.
	 *
	 * @return bool
	 */
	public function can_view() {
		return true;
	}

	/**
	 * Check if task is disabled.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return bool
	 */
	public function is_disabled() {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		return false;
	}

	/**
	 * Check if the task is complete.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return self::is_actioned();
	}

	/**
	 * Check if the task has been visited.
	 *
	 * @return bool
	 */
	public function is_visited() {
		$user_id       = get_current_user_id();
		$response      = WCAdminUser::get_user_data_field( $user_id, 'task_list_tracked_started_tasks' );
		$tracked_tasks = $response ? json_decode( $response, true ) : array();

		return isset( $tracked_tasks[ $this->get_id() ] ) && $tracked_tasks[ $this->get_id() ] > 0;
	}

	/**
	 * Check if should record event when task is viewed
	 *
	 * @return bool
	 */
	public function get_record_view_event(): bool {
		return false;
	}

	/**
	 * Get the task as JSON.
	 *
	 * @return array
	 */
	public function get_json() {
		$this->possibly_track_completion();

		return array(
			'id'              => $this->get_id(),
			'parentId'        => $this->get_parent_id(),
			'title'           => $this->get_title(),
			'badge'           => $this->get_badge(),
			'canView'         => $this->can_view(),
			'content'         => $this->get_content(),
			'additionalInfo'  => $this->get_additional_info(),
			'actionLabel'     => $this->get_action_label(),
			'actionUrl'       => $this->get_action_url(),
			'isComplete'      => $this->is_complete(),
			'time'            => $this->get_time(),
			'level'           => 3,
			'isActioned'      => $this->is_actioned(),
			'isDismissed'     => $this->is_dismissed(),
			'isDismissable'   => $this->is_dismissable(),
			'isSnoozed'       => false,
			'isSnoozeable'    => false,
			'isVisited'       => $this->is_visited(),
			'isDisabled'      => false,
			'snoozedUntil'    => null,
			'additionalData'  => self::convert_object_to_camelcase( $this->get_additional_data() ),
			'eventPrefix'     => $this->prefix_event( '' ),
			'recordViewEvent' => $this->get_record_view_event(),
		);
	}

	/**
	 * Convert object keys to camelcase.
	 *
	 * @param array $data Data to convert.
	 * @return object
	 */
	public static function convert_object_to_camelcase( $data ) {
		if ( ! is_array( $data ) ) {
			return $data;
		}

		$new_object = (object) array();

		foreach ( $data as $key => $value ) {
			$new_key              = lcfirst( implode( '', array_map( 'ucfirst', explode( '_', $key ) ) ) );
			$new_object->$new_key = $value;
		}

		return $new_object;
	}

	/**
	 * Mark a task as actioned.  Used to verify an action has taken place in some tasks.
	 *
	 * @return bool
	 */
	public function mark_actioned() {
		$actioned = get_option( self::ACTIONED_OPTION, array() );

		$actioned[] = $this->get_id();
		$update     = update_option( self::ACTIONED_OPTION, array_unique( $actioned ) );

		if ( $update ) {
			$this->record_tracks_event( 'actioned_task', array( 'task_name' => $this->get_id() ) );
		}

		return $update;
	}

	/**
	 * Check if a task has been actioned.
	 *
	 * @return bool
	 */
	public function is_actioned() {
		return self::is_task_actioned( $this->get_id() );
	}

	/**
	 * Check if a provided task ID has been actioned.
	 *
	 * @param string $id Task ID.
	 * @return bool
	 */
	public static function is_task_actioned( $id ) {
		$actioned = get_option( self::ACTIONED_OPTION, array() );
		return in_array( $id, $actioned, true );
	}

	/**
	 * Sorting function for tasks.
	 *
	 * @param Task  $a Task a.
	 * @param Task  $b Task b.
	 * @param array $sort_by list of columns with sort order.
	 * @return int
	 */
	public static function sort( $a, $b, $sort_by = array() ) {
		$result = 0;
		foreach ( $sort_by as $data ) {
			$key   = $data['key'];
			$a_val = $a->$key ?? false;
			$b_val = $b->$key ?? false;
			if ( 'asc' === $data['order'] ) {
				$result = $a_val <=> $b_val;
			} else {
				$result = $b_val <=> $a_val;
			}

			if ( 0 !== $result ) {
				break;
			}
		}
		return $result;
	}

}
OnboardingTasks/TaskList.php000064400000022162151542725270012120 0ustar00<?php
/**
 * Handles storage and retrieval of a task list
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Admin\WCAdminHelper;


/**
 * Task List class.
 */
class TaskList {
	/**
	 * Task traits.
	 */
	use TaskTraits;

	/**
	 * Option name hidden task lists.
	 */
	const HIDDEN_OPTION = 'woocommerce_task_list_hidden_lists';

	/**
	 * Option name of completed task lists.
	 */
	const COMPLETED_OPTION = 'woocommerce_task_list_completed_lists';

	/**
	 * Option name of hidden reminder bar.
	 */
	const REMINDER_BAR_HIDDEN_OPTION = 'woocommerce_task_list_reminder_bar_hidden';

	/**
	 * ID.
	 *
	 * @var string
	 */
	public $id = '';

	/**
	 * ID.
	 *
	 * @var string
	 */
	public $hidden_id = '';

	/**
	 * ID.
	 *
	 * @var boolean
	 */
	public $display_progress_header = false;

	/**
	 * Title.
	 *
	 * @var string
	 */
	public $title = '';

	/**
	 * Tasks.
	 *
	 * @var array
	 */
	public $tasks = array();

	/**
	 * Sort keys.
	 *
	 * @var array
	 */
	public $sort_by = array();

	/**
	 * Event prefix.
	 *
	 * @var string|null
	 */
	public $event_prefix = null;

	/**
	 * Task list visibility.
	 *
	 * @var boolean
	 */
	public $visible = true;

	/**
	 * Array of custom options.
	 *
	 * @var array
	 */
	public $options = array();

	/**
	 * Array of TaskListSection.
	 *
	 * @deprecated 7.2.0
	 *
	 * @var array
	 */
	private $sections = array();

	/**
	 * Key value map of task class and id used for sections.
	 *
	 * @deprecated 7.2.0
	 *
	 * @var array
	 */
	public $task_class_id_map = array();

	/**
	 * Constructor
	 *
	 * @param array $data Task list data.
	 */
	public function __construct( $data = array() ) {
		$defaults = array(
			'id'                      => null,
			'hidden_id'               => null,
			'title'                   => '',
			'tasks'                   => array(),
			'sort_by'                 => array(),
			'event_prefix'            => null,
			'options'                 => array(),
			'visible'                 => true,
			'display_progress_header' => false,
		);

		$data = wp_parse_args( $data, $defaults );

		$this->id                      = $data['id'];
		$this->hidden_id               = $data['hidden_id'];
		$this->title                   = $data['title'];
		$this->sort_by                 = $data['sort_by'];
		$this->event_prefix            = $data['event_prefix'];
		$this->options                 = $data['options'];
		$this->visible                 = $data['visible'];
		$this->display_progress_header = $data['display_progress_header'];

		foreach ( $data['tasks'] as $task_name ) {
			$class = 'Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\\' . $task_name;
			$task  = new $class( $this );
			$this->add_task( $task );
		}

		$this->possibly_remove_reminder_bar();
	}

	/**
	 * Check if the task list is hidden.
	 *
	 * @return bool
	 */
	public function is_hidden() {
		$hidden = get_option( self::HIDDEN_OPTION, array() );
		return in_array( $this->hidden_id ? $this->hidden_id : $this->id, $hidden, true );
	}

	/**
	 * Check if the task list is visible.
	 *
	 * @return bool
	 */
	public function is_visible() {
		if ( ! $this->visible || ! count( $this->get_viewable_tasks() ) > 0 ) {
			return false;
		}
		return ! $this->is_hidden();
	}

	/**
	 * Hide the task list.
	 *
	 * @return bool
	 */
	public function hide() {
		if ( $this->is_hidden() ) {
			return;
		}

		$viewable_tasks  = $this->get_viewable_tasks();
		$completed_count = array_reduce(
			$viewable_tasks,
			function( $total, $task ) {
				return $task->is_complete() ? $total + 1 : $total;
			},
			0
		);

		$this->record_tracks_event(
			'completed',
			array(
				'action'                => 'remove_card',
				'completed_task_count'  => $completed_count,
				'incomplete_task_count' => count( $viewable_tasks ) - $completed_count,
			)
		);

		$hidden   = get_option( self::HIDDEN_OPTION, array() );
		$hidden[] = $this->hidden_id ? $this->hidden_id : $this->id;
		$this->maybe_set_default_layout( $hidden );
		return update_option( self::HIDDEN_OPTION, array_unique( $hidden ) );
	}

	/**
	 * Sets the default homepage layout to two_columns if "setup" tasklist is completed or hidden.
	 *
	 * @param array $completed_or_hidden_tasklist_ids Array of tasklist ids.
	 */
	public function maybe_set_default_layout( $completed_or_hidden_tasklist_ids ) {
		if ( in_array( 'setup', $completed_or_hidden_tasklist_ids, true ) ) {
			update_option( 'woocommerce_default_homepage_layout', 'two_columns' );
		}
	}

	/**
	 * Undo hiding of the task list.
	 *
	 * @return bool
	 */
	public function unhide() {
		$hidden = get_option( self::HIDDEN_OPTION, array() );
		$hidden = array_diff( $hidden, array( $this->hidden_id ? $this->hidden_id : $this->id ) );
		return update_option( self::HIDDEN_OPTION, $hidden );
	}

	/**
	 * Check if all viewable tasks are complete.
	 *
	 * @return bool
	 */
	public function is_complete() {
		foreach ( $this->get_viewable_tasks() as $viewable_task ) {
			if ( $viewable_task->is_complete() === false ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Check if a task list has previously been marked as complete.
	 *
	 * @return bool
	 */
	public function has_previously_completed() {
		$complete = get_option( self::COMPLETED_OPTION, array() );
		return in_array( $this->get_list_id(), $complete, true );
	}

	/**
	 * Add task to the task list.
	 *
	 * @param Task $task Task class.
	 */
	public function add_task( $task ) {
		if ( ! is_subclass_of( $task, 'Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task' ) ) {
			return new \WP_Error(
				'woocommerce_task_list_invalid_task',
				__( 'Task is not a subclass of `Task`', 'woocommerce' )
			);
		}
		if ( array_search( $task, $this->tasks, true ) ) {
			return;
		}

		$this->tasks[] = $task;
	}

	/**
	 * Get only visible tasks in list.
	 *
	 * @param string $task_id id of task.
	 * @return Task
	 */
	public function get_task( $task_id ) {
		return current(
			array_filter(
				$this->tasks,
				function( $task ) use ( $task_id ) {
					return $task->get_id() === $task_id;
				}
			)
		);
	}

	/**
	 * Get only visible tasks in list.
	 *
	 * @return array
	 */
	public function get_viewable_tasks() {
		return array_values(
			array_filter(
				$this->tasks,
				function( $task ) {
					return $task->can_view();
				}
			)
		);
	}

	/**
	 * Get task list sections.
	 *
	 * @deprecated 7.2.0
	 *
	 * @return array
	 */
	public function get_sections() {
		wc_deprecated_function( __CLASS__ . '::' . __FUNCTION__, '7.2.0' );

		return $this->sections;
	}

	/**
	 * Track list completion of viewable tasks.
	 */
	public function possibly_track_completion() {
		if ( ! $this->is_complete() ) {
			return;
		}

		if ( $this->has_previously_completed() ) {
			return;
		}

		$completed_lists   = get_option( self::COMPLETED_OPTION, array() );
		$completed_lists[] = $this->get_list_id();
		update_option( self::COMPLETED_OPTION, $completed_lists );
		$this->maybe_set_default_layout( $completed_lists );
		$this->record_tracks_event( 'tasks_completed' );
	}

	/**
	 * Sorts the attached tasks array.
	 *
	 * @param array $sort_by list of columns with sort order.
	 * @return TaskList returns $this, for chaining.
	 */
	public function sort_tasks( $sort_by = array() ) {
		$sort_by = count( $sort_by ) > 0 ? $sort_by : $this->sort_by;
		if ( 0 !== count( $sort_by ) ) {
			usort(
				$this->tasks,
				function( $a, $b ) use ( $sort_by ) {
					return Task::sort( $a, $b, $sort_by );
				}
			);
		}
		return $this;
	}

	/**
	 * Prefix event for track event naming.
	 *
	 * @param string $event_name Event name.
	 * @return string
	 */
	public function prefix_event( $event_name ) {
		if ( null !== $this->event_prefix ) {
			return $this->event_prefix . $event_name;
		}
		return $this->get_list_id() . '_tasklist_' . $event_name;
	}

	/**
	 * Returns option to keep completed task list.
	 *
	 * @return string
	 */
	public function get_keep_completed_task_list() {
		return get_option( 'woocommerce_task_list_keep_completed', 'no' );
	}

	/**
	 * Remove reminder bar four weeks after store creation.
	 */
	public static function possibly_remove_reminder_bar() {
		$bar_hidden            = get_option( self::REMINDER_BAR_HIDDEN_OPTION, 'no' );
		$active_for_four_weeks = WCAdminHelper::is_wc_admin_active_for( WEEK_IN_SECONDS * 4 );

		if ( 'yes' === $bar_hidden || ! $active_for_four_weeks ) {
			return;
		}

		update_option( self::REMINDER_BAR_HIDDEN_OPTION, 'yes' );
	}

	/**
	 * Get the list for use in JSON.
	 *
	 * @return array
	 */
	public function get_json() {
		$this->possibly_track_completion();
		$tasks_json = array();
		foreach ( $this->tasks as $task ) {
			$json = $task->get_json();
			if ( $json['canView'] ) {
				$tasks_json[] = $json;
			}
		}

		return array(
			'id'                    => $this->get_list_id(),
			'title'                 => $this->title,
			'isHidden'              => $this->is_hidden(),
			'isVisible'             => $this->is_visible(),
			'isComplete'            => $this->is_complete(),
			'tasks'                 => $tasks_json,
			'eventPrefix'           => $this->prefix_event( '' ),
			'displayProgressHeader' => $this->display_progress_header,
			'keepCompletedTaskList' => $this->get_keep_completed_task_list(),
		);
	}
}
OnboardingTasks/TaskListSection.php000064400000004466151542725270013454 0ustar00<?php
/**
 * Handles storage and retrieval of a task list section
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

/**
 * Task List section class.
 *
 * @deprecated 7.2.0
 */
class TaskListSection {

	/**
	 * Title.
	 *
	 * @var string
	 */
	public $id = '';

	/**
	 * Title.
	 *
	 * @var string
	 */
	public $title = '';

	/**
	 * Description.
	 *
	 * @var string
	 */
	public $description = '';

	/**
	 * Image.
	 *
	 * @var string
	 */
	public $image = '';

	/**
	 * Tasks.
	 *
	 * @var array
	 */
	public $task_names = array();

	/**
	 * Parent task list.
	 *
	 * @var TaskList
	 */
	protected $task_list;

	/**
	 * Constructor
	 *
	 * @param array         $data Task list data.
	 * @param TaskList|null $task_list Parent task list.
	 */
	public function __construct( $data = array(), $task_list = null ) {
		$defaults = array(
			'id'          => '',
			'title'       => '',
			'description' => '',
			'image'       => '',
			'tasks'       => array(),
		);

		$data = wp_parse_args( $data, $defaults );

		$this->task_list   = $task_list;
		$this->id          = $data['id'];
		$this->title       = $data['title'];
		$this->description = $data['description'];
		$this->image       = $data['image'];
		$this->task_names  = $data['task_names'];
	}

	/**
	 * Returns if section is complete.
	 *
	 * @return boolean;
	 */
	private function is_complete() {
		$complete = true;
		foreach ( $this->task_names as $task_name ) {
			if ( null !== $this->task_list && isset( $this->task_list->task_class_id_map[ $task_name ] ) ) {
				$task = $this->task_list->get_task( $this->task_list->task_class_id_map[ $task_name ] );
				if ( $task->can_view() && ! $task->is_complete() ) {
					$complete = false;
					break;
				}
			}
		}
		return $complete;
	}

	/**
	 * Get the list for use in JSON.
	 *
	 * @return array
	 */
	public function get_json() {
		return array(
			'id'          => $this->id,
			'title'       => $this->title,
			'description' => $this->description,
			'image'       => $this->image,
			'tasks'       => array_map(
				function( $task_name ) {
					if ( null !== $this->task_list && isset( $this->task_list->task_class_id_map[ $task_name ] ) ) {
						return $this->task_list->task_class_id_map[ $task_name ];
					}
					return '';
				},
				$this->task_names
			),
			'isComplete'  => $this->is_complete(),
		);
	}
}
OnboardingTasks/TaskLists.php000064400000025177151542725270012314 0ustar00<?php
/**
 * Handles storage and retrieval of task lists
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\DeprecatedExtendedTask;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\ReviewShippingOptions;
/**
 * Task Lists class.
 */
class TaskLists {
	/**
	 * Class instance.
	 *
	 * @var TaskLists instance
	 */
	protected static $instance = null;

	/**
	 * An array of all registered lists.
	 *
	 * @var array
	 */
	protected static $lists = array();

	/**
	 * Boolean value to indicate if default tasks have been added.
	 *
	 * @var boolean
	 */
	protected static $default_tasks_loaded = false;

	/**
	 * The contents of this array is used in init_tasks() to run their init() methods.
	 * If the classes do not have an init() method then nothing is executed.
	 * Beyond that, adding tasks to this list has no effect, see init_default_lists() for the list of tasks.
	 * that are added for each task list.
	 *
	 * @var array
	 */
	const DEFAULT_TASKS = array(
		'StoreDetails',
		'Products',
		'WooCommercePayments',
		'Payments',
		'Tax',
		'Shipping',
		'Marketing',
		'Appearance',
		'AdditionalPayments',
		'ReviewShippingOptions',
		'GetMobileApp',
	);

	/**
	 * Get class instance.
	 */
	final public static function instance() {
		if ( ! static::$instance ) {
			static::$instance = new static();
		}
		return static::$instance;
	}

	/**
	 * Initialize the task lists.
	 */
	public static function init() {
		self::init_default_lists();
		add_action( 'admin_init', array( __CLASS__, 'set_active_task' ), 5 );
		add_action( 'init', array( __CLASS__, 'init_tasks' ) );
		add_action( 'admin_menu', array( __CLASS__, 'menu_task_count' ) );
		add_filter( 'woocommerce_admin_shared_settings', array( __CLASS__, 'task_list_preloaded_settings' ), 20 );
	}

	/**
	 * Check if an experiment is the treatment or control.
	 *
	 * @param string $name Name prefix of experiment.
	 * @return bool
	 */
	public static function is_experiment_treatment( $name ) {
		$anon_id        = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : '';
		$allow_tracking = 'yes' === get_option( 'woocommerce_allow_tracking' );
		$abtest         = new \WooCommerce\Admin\Experimental_Abtest(
			$anon_id,
			'woocommerce',
			$allow_tracking
		);

		$date = new \DateTime();
		$date->setTimeZone( new \DateTimeZone( 'UTC' ) );

		$experiment_name = sprintf(
			'%s_%s_%s',
			$name,
			$date->format( 'Y' ),
			$date->format( 'm' )
		);
		return $abtest->get_variation( $experiment_name ) === 'treatment';
	}

	/**
	 * Initialize default lists.
	 */
	public static function init_default_lists() {
		$tasks = array(
			'CustomizeStore',
			'StoreDetails',
			'Products',
			'Appearance',
			'WooCommercePayments',
			'Payments',
			'Tax',
			'Shipping',
			'Marketing',
		);

		if ( Features::is_enabled( 'core-profiler' ) ) {
			$key = array_search( 'StoreDetails', $tasks, true );
			if ( false !== $key ) {
				unset( $tasks[ $key ] );
			}
		}

		// Remove the old Personalize your store task if the new CustomizeStore is enabled.
		$task_to_remove                 = Features::is_enabled( 'customize-store' ) ? 'Appearance' : 'CustomizeStore';
		$store_customisation_task_index = array_search( $task_to_remove, $tasks, true );

		if ( false !== $store_customisation_task_index ) {
			unset( $tasks[ $store_customisation_task_index ] );
		}

		self::add_list(
			array(
				'id'                      => 'setup',
				'title'                   => __( 'Get ready to start selling', 'woocommerce' ),
				'tasks'                   => $tasks,
				'display_progress_header' => true,
				'event_prefix'            => 'tasklist_',
				'options'                 => array(
					'use_completed_title' => true,
				),
				'visible'                 => true,
			)
		);

		self::add_list(
			array(
				'id'      => 'extended',
				'title'   => __( 'Things to do next', 'woocommerce' ),
				'sort_by' => array(
					array(
						'key'   => 'is_complete',
						'order' => 'asc',
					),
					array(
						'key'   => 'level',
						'order' => 'asc',
					),
				),
				'tasks'   => array(
					'AdditionalPayments',
					'GetMobileApp',
				),
			)
		);

		if ( Features::is_enabled( 'shipping-smart-defaults' ) ) {
			self::add_task(
				'extended',
				new ReviewShippingOptions(
					self::get_list( 'extended' )
				)
			);

			// Tasklist that will never be shown in homescreen,
			// used for having tasks that are accessed by other means.
			self::add_list(
				array(
					'id'           => 'secret_tasklist',
					'hidden_id'    => 'setup',
					'tasks'        => array(
						'ExperimentalShippingRecommendation',
					),
					'event_prefix' => 'secret_tasklist_',
					'visible'      => false,
				)
			);
		}

		if ( has_filter( 'woocommerce_admin_experimental_onboarding_tasklists' ) ) {
			/**
			 * Filter to override default task lists.
			 *
			 * @since 7.4
			 * @param array     $lists Array of tasklists.
			 */
			self::$lists = apply_filters( 'woocommerce_admin_experimental_onboarding_tasklists', self::$lists );
		}
	}

	/**
	 * Initialize tasks.
	 */
	public static function init_tasks() {
		foreach ( self::DEFAULT_TASKS as $task ) {
			$class = 'Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\\' . $task;
			if ( ! method_exists( $class, 'init' ) ) {
				continue;
			}
			$class::init();
		}
	}

	/**
	 * Temporarily store the active task to persist across page loads when necessary.
	 * Most tasks do not need this.
	 */
	public static function set_active_task() {
		if ( ! isset( $_GET[ Task::ACTIVE_TASK_TRANSIENT ] ) || ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore csrf ok.
			return;
		}
		$referer = wp_get_referer();
		if ( ! $referer || 0 !== strpos( $referer, wc_admin_url() ) ) {
			return;
		}

		$task_id = sanitize_title_with_dashes( wp_unslash( $_GET[ Task::ACTIVE_TASK_TRANSIENT ] ) ); // phpcs:ignore csrf ok.

		$task = self::get_task( $task_id );

		if ( ! $task ) {
			return;
		}

		$task->set_active();
	}

	/**
	 * Add a task list.
	 *
	 * @param array $args Task list properties.
	 * @return \WP_Error|TaskList
	 */
	public static function add_list( $args ) {
		if ( isset( self::$lists[ $args['id'] ] ) ) {
			return new \WP_Error(
				'woocommerce_task_list_exists',
				__( 'Task list ID already exists', 'woocommerce' )
			);
		}

		self::$lists[ $args['id'] ] = new TaskList( $args );
		return self::$lists[ $args['id'] ];
	}

	/**
	 * Add task to a given task list.
	 *
	 * @param string $list_id List ID to add the task to.
	 * @param Task   $task Task object.
	 *
	 * @return \WP_Error|Task
	 */
	public static function add_task( $list_id, $task ) {
		if ( ! isset( self::$lists[ $list_id ] ) ) {
			return new \WP_Error(
				'woocommerce_task_list_invalid_list',
				__( 'Task list ID does not exist', 'woocommerce' )
			);
		}

		self::$lists[ $list_id ]->add_task( $task );
	}

	/**
	 * Add default extended task lists.
	 *
	 * @param array $extended_tasks list of extended tasks.
	 */
	public static function maybe_add_extended_tasks( $extended_tasks ) {
		$tasks = $extended_tasks ?? array();

		foreach ( self::$lists as $task_list ) {
			if ( 'extended' !== substr( $task_list->id, 0, 8 ) ) {
				continue;
			}
			foreach ( $tasks as $args ) {
				$task = new DeprecatedExtendedTask( $task_list, $args );
				$task_list->add_task( $task );
			}
		}

	}

	/**
	 * Get all task lists.
	 *
	 * @return array
	 */
	public static function get_lists() {
		return self::$lists;
	}

	/**
	 * Get all task lists.
	 *
	 * @param array $ids list of task list ids.
	 * @return array
	 */
	public static function get_lists_by_ids( $ids ) {
		return array_filter(
			self::$lists,
			function( $list ) use ( $ids ) {
				return in_array( $list->get_list_id(), $ids, true );
			}
		);
	}

	/**
	 * Get all task list ids.
	 *
	 * @return array
	 */
	public static function get_list_ids() {
		return array_keys( self::$lists );
	}

	/**
	 * Clear all task lists.
	 */
	public static function clear_lists() {
		self::$lists = array();
		return self::$lists;
	}

	/**
	 * Get visible task lists.
	 */
	public static function get_visible() {
		return array_filter(
			self::get_lists(),
			function ( $task_list ) {
				return $task_list->is_visible();
			}
		);
	}


	/**
	 * Retrieve a task list by ID.
	 *
	 * @param String $id Task list ID.
	 *
	 * @return TaskList|null
	 */
	public static function get_list( $id ) {
		if ( isset( self::$lists[ $id ] ) ) {
			return self::$lists[ $id ];
		}

		return null;
	}

	/**
	 * Retrieve single task.
	 *
	 * @param String $id Task ID.
	 * @param String $task_list_id Task list ID.
	 *
	 * @return Object
	 */
	public static function get_task( $id, $task_list_id = null ) {
		$task_list = $task_list_id ? self::get_list( $task_list_id ) : null;

		if ( $task_list_id && ! $task_list ) {
			return null;
		}

		$tasks_to_search = $task_list ? $task_list->tasks : array_reduce(
			self::get_lists(),
			function ( $all, $curr ) {
				return array_merge( $all, $curr->tasks );
			},
			array()
		);

		foreach ( $tasks_to_search as $task ) {
			if ( $id === $task->get_id() ) {
				return $task;
			}
		}

		return null;
	}

	/**
	 * Return number of setup tasks remaining
	 *
	 * @return number
	 */
	public static function setup_tasks_remaining() {
		$setup_list = self::get_list( 'setup' );

		if ( ! $setup_list || $setup_list->is_hidden() || $setup_list->is_complete() ) {
			return;
		}

		$remaining_tasks = array_values(
			array_filter(
				$setup_list->get_viewable_tasks(),
				function( $task ) {
					return ! $task->is_complete();
				}
			)
		);

		return count( $remaining_tasks );
	}

	/**
	 * Add badge to homescreen menu item for remaining tasks
	 */
	public static function menu_task_count() {
		global $submenu;

		$tasks_count = self::setup_tasks_remaining();

		if ( ! $tasks_count || ! isset( $submenu['woocommerce'] ) ) {
			return;
		}

		foreach ( $submenu['woocommerce'] as $key => $menu_item ) {
			if ( 0 === strpos( $menu_item[0], _x( 'Home', 'Admin menu name', 'woocommerce' ) ) ) {
				$submenu['woocommerce'][ $key ][0] .= ' <span class="awaiting-mod update-plugins remaining-tasks-badge woocommerce-task-list-remaining-tasks-badge"><span class="count-' . esc_attr( $tasks_count ) . '">' . absint( $tasks_count ) . '</span></span>'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
				break;
			}
		}

	}

	/**
	 * Add visible list ids to component settings.
	 *
	 * @param array $settings Component settings.
	 *
	 * @return array
	 */
	public static function task_list_preloaded_settings( $settings ) {
		$settings['visibleTaskListIds'] = array_keys( self::get_visible() );

		return $settings;
	}
}
OnboardingTasks/TaskTraits.php000064400000001713151542725270012452 0ustar00<?php
/**
 * Task and TaskList Traits
 */

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks;

defined( 'ABSPATH' ) || exit;

/**
 * TaskTraits class.
 */
trait TaskTraits {
	/**
	 * Record a tracks event with the prefixed event name.
	 *
	 * @param string $event_name Event name.
	 * @param array  $args Array of tracks arguments.
	 * @return string Prefixed event name.
	 */
	public function record_tracks_event( $event_name, $args = array() ) {
		if ( ! $this->get_list_id() ) {
			return;
		}

		$prefixed_event_name = $this->prefix_event( $event_name );

		wc_admin_record_tracks_event(
			$prefixed_event_name,
			$args
		);

		return $prefixed_event_name;
	}

	/**
	 * Get the task list ID.
	 *
	 * @return string
	 */
	public function get_list_id() {
		$namespaced_class = get_class( $this );
		return is_subclass_of( $namespaced_class, 'Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task' )
			? $this->get_parent_id()
			: $this->id;
	}
}
OnboardingTasks/Tasks/AdditionalPayments.php000064400000011066151542725270015241 0ustar00<?php


namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Payments;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\WooCommercePayments;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\Init;

/**
 * Payments Task
 */
class AdditionalPayments extends Payments {

	/**
	 * Used to cache is_complete() method result.
	 *
	 * @var null
	 */
	private $is_complete_result = null;

	/**
	 * Used to cache can_view() method result.
	 *
	 * @var null
	 */
	private $can_view_result = null;


	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'payments';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		return __(
			'Set up additional payment options',
			'woocommerce'
		);
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			'Choose payment providers and enable payment methods at checkout.',
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '2 minutes', 'woocommerce' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		if ( null === $this->is_complete_result ) {
			$this->is_complete_result = self::has_enabled_additional_gateways();
		}

		return $this->is_complete_result;
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		if ( ! Features::is_enabled( 'payment-gateway-suggestions' ) ) {
			// Hide task if feature not enabled.
			return false;
		}

		if ( null !== $this->can_view_result ) {
			return $this->can_view_result;
		}

		// Show task if woocommerce-payments is connected or if there are any suggested gateways in other category enabled.
		$this->can_view_result = (
			WooCommercePayments::is_connected() ||
			self::has_enabled_other_category_gateways()
		);

		// Early return if task is not visible.
		if ( ! $this->can_view_result ) {
			return false;
		}

		// Show task if there are any suggested gateways in additional category.
		$this->can_view_result = ! empty( self::get_suggestion_gateways( 'category_additional' ) );

		return $this->can_view_result;
	}


	/**
	 * Check if the store has any enabled gateways in other category.
	 *
	 * @return bool
	 */
	private static function has_enabled_other_category_gateways() {
		$other_gateways     = self::get_suggestion_gateways( 'category_other' );
		$other_gateways_ids = wp_list_pluck( $other_gateways, 'id' );

		return self::has_enabled_gateways(
			function( $gateway ) use ( $other_gateways_ids ) {
				return in_array( $gateway->id, $other_gateways_ids, true );
			}
		);
	}

	/**
	 * Check if the store has any enabled gateways in additional category.
	 *
	 * @return bool
	 */
	private static function has_enabled_additional_gateways() {
		$additional_gateways     = self::get_suggestion_gateways( 'category_additional' );
		$additional_gateways_ids = wp_list_pluck( $additional_gateways, 'id' );

		return self::has_enabled_gateways(
			function( $gateway ) use ( $additional_gateways_ids ) {
				return 'yes' === $gateway->enabled
				&& in_array( $gateway->id, $additional_gateways_ids, true );
			}
		);
	}

	/**
	 * Check if the store has any enabled gateways based on the given criteria.
	 *
	 * @param callable|null $filter A callback function to filter the gateways.
	 * @return bool
	 */
	private static function has_enabled_gateways( $filter = null ) {
		$gateways         = WC()->payment_gateways->get_available_payment_gateways();
		$enabled_gateways = array_filter(
			$gateways,
			function( $gateway ) use ( $filter ) {
				if ( is_callable( $filter ) ) {
					return 'yes' === $gateway->enabled && call_user_func( $filter, $gateway );
				} else {
					return 'yes' === $gateway->enabled;
				}
			}
		);

		return ! empty( $enabled_gateways );
	}

	/**
	 * Get the list of gateways to suggest.
	 *
	 * @param string $filter_by Filter by category. "category_additional" or "category_other".
	 *
	 * @return array
	 */
	private static function get_suggestion_gateways( $filter_by = 'category_additional' ) {
		$country            = wc_get_base_location()['country'];
		$plugin_suggestions = Init::get_suggestions();
		$plugin_suggestions = array_filter(
			$plugin_suggestions,
			function( $plugin ) use ( $country, $filter_by ) {
				if ( ! isset( $plugin->{$filter_by} ) || ! isset( $plugin->plugins[0] ) ) {
					return false;
				}
				return in_array( $country, $plugin->{$filter_by}, true );
			}
		);
		return $plugin_suggestions;
	}
}
OnboardingTasks/Tasks/Appearance.php000064400000002513151542725270013504 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Internal\Admin\Loader;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\Products;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;

/**
 * Appearance Task
 */
class Appearance extends Task {

	/**
	 * Constructor.
	 */
	public function __construct() {
		if ( ! $this->is_complete() ) {
			add_action( 'load-theme-install.php', array( $this, 'mark_actioned' ) );
		}
	}

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'appearance';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		return __( 'Choose your theme', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			"Choose a theme that best fits your brand's look and feel, then make it your own. Change the colors, add your logo, and create pages.",
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '2 minutes', 'woocommerce' );
	}

	/**
	 * Action label.
	 *
	 * @return string
	 */
	public function get_action_label() {
		return __( 'Choose theme', 'woocommerce' );
	}
}
OnboardingTasks/Tasks/CustomizeStore.php000064400000013237151542725270014451 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Jetpack_Gutenberg;

/**
 * Customize Your Store Task
 */
class CustomizeStore extends Task {
	/**
	 * Constructor
	 *
	 * @param TaskList $task_list Parent task list.
	 */
	public function __construct( $task_list ) {
		parent::__construct( $task_list );

		add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_site_editor_scripts' ) );
		add_action( 'after_switch_theme', array( $this, 'mark_task_as_complete' ) );
	}

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'customize-store';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		return __( 'Customize your store ', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return '';
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return '';
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return get_option( 'woocommerce_admin_customize_store_completed' ) === 'yes';
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		return true;
	}

	/**
	 * Possibly add site editor scripts.
	 */
	public function possibly_add_site_editor_scripts() {
		$is_customize_store_pages = (
			isset( $_GET['page'] ) &&
			'wc-admin' === $_GET['page'] &&
			isset( $_GET['path'] ) &&
			str_starts_with( wc_clean( wp_unslash( $_GET['path'] ) ), '/customize-store' )
		);
		if ( ! $is_customize_store_pages ) {
			return;
		}

		// See: https://github.com/WordPress/WordPress/blob/master/wp-admin/site-editor.php.
		if ( ! wp_is_block_theme() ) {
			wp_die( esc_html__( 'The theme you are currently using is not compatible.', 'woocommerce' ) );
		}
		global $editor_styles;

		// Flag that we're loading the block editor.
		$current_screen = get_current_screen();
		$current_screen->is_block_editor( true );

		// Default to is-fullscreen-mode to avoid jumps in the UI.
		add_filter(
			'admin_body_class',
			static function( $classes ) {
				return "$classes is-fullscreen-mode";
			}
		);

		$block_editor_context   = new \WP_Block_Editor_Context( array( 'name' => 'core/edit-site' ) );
		$indexed_template_types = array();
		foreach ( get_default_block_template_types() as $slug => $template_type ) {
			$template_type['slug']    = (string) $slug;
			$indexed_template_types[] = $template_type;
		}

		$custom_settings = array(
			'siteUrl'                   => site_url(),
			'postsPerPage'              => get_option( 'posts_per_page' ),
			'styles'                    => get_block_editor_theme_styles(),
			'defaultTemplateTypes'      => $indexed_template_types,
			'defaultTemplatePartAreas'  => get_allowed_block_template_part_areas(),
			'supportsLayout'            => wp_theme_has_theme_json(),
			'supportsTemplatePartsMode' => ! wp_is_block_theme() && current_theme_supports( 'block-template-parts' ),
		);

		// Add additional back-compat patterns registered by `current_screen` et al.
		$custom_settings['__experimentalAdditionalBlockPatterns']          = \WP_Block_Patterns_Registry::get_instance()->get_all_registered( true );
		$custom_settings['__experimentalAdditionalBlockPatternCategories'] = \WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered( true );

		$editor_settings         = get_block_editor_settings( $custom_settings, $block_editor_context );
		$active_global_styles_id = \WP_Theme_JSON_Resolver::get_user_global_styles_post_id();
		$active_theme            = get_stylesheet();
		$preload_paths           = array(
			array( '/wp/v2/media', 'OPTIONS' ),
			'/wp/v2/types?context=view',
			'/wp/v2/types/wp_template?context=edit',
			'/wp/v2/types/wp_template-part?context=edit',
			'/wp/v2/templates?context=edit&per_page=-1',
			'/wp/v2/template-parts?context=edit&per_page=-1',
			'/wp/v2/themes?context=edit&status=active',
			'/wp/v2/global-styles/' . $active_global_styles_id . '?context=edit',
			'/wp/v2/global-styles/' . $active_global_styles_id,
			'/wp/v2/global-styles/themes/' . $active_theme,
		);

		block_editor_rest_api_preload( $preload_paths, $block_editor_context );

		wp_add_inline_script(
			'wp-blocks',
			sprintf(
				'window.wcBlockSettings = %s;',
				wp_json_encode( $editor_settings )
			)
		);

		// Preload server-registered block schemas.
		wp_add_inline_script(
			'wp-blocks',
			'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( get_block_editor_server_block_settings() ) . ');'
		);

		wp_add_inline_script(
			'wp-blocks',
			sprintf( 'wp.blocks.setCategories( %s );', wp_json_encode( isset( $editor_settings['blockCategories'] ) ? $editor_settings['blockCategories'] : array() ) ),
			'after'
		);

		wp_enqueue_script( 'wp-editor' );
		wp_enqueue_script( 'wp-format-library' ); // Not sure if this is needed.
		wp_enqueue_script( 'wp-router' );
		wp_enqueue_style( 'wp-editor' );
		wp_enqueue_style( 'wp-edit-site' );
		wp_enqueue_style( 'wp-format-library' );
		wp_enqueue_media();

		if (
				current_theme_supports( 'wp-block-styles' ) &&
				( ! is_array( $editor_styles ) || count( $editor_styles ) === 0 )
			) {
			wp_enqueue_style( 'wp-block-library-theme' );
		}
		/** This action is documented in wp-admin/edit-form-blocks.php
		 *
		 * @since 8.0.3
		*/
		do_action( 'enqueue_block_editor_assets' );

		// Load Jetpack's block editor assets because they are not enqueued by default.
		if ( class_exists( 'Jetpack_Gutenberg' ) ) {
			Jetpack_Gutenberg::enqueue_block_editor_assets();
		}
	}

	/**
	 * Mark task as complete.
	 */
	public function mark_task_as_complete() {
		update_option( 'woocommerce_admin_customize_store_completed', 'yes' );
	}
}
OnboardingTasks/Tasks/ExperimentalShippingRecommendation.php000064400000003216151542725270020472 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Admin\PluginsHelper;

/**
 * Shipping Task
 */
class ExperimentalShippingRecommendation extends Task {
	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'shipping-recommendation';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		return __( 'Set up shipping', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return '';
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return '';
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return self::has_plugins_active() && self::has_jetpack_connected();
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		return Features::is_enabled( 'shipping-smart-defaults' );
	}

	/**
	 * Action URL.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return '';
	}

	/**
	 * Check if the store has any shipping zones.
	 *
	 * @return bool
	 */
	public static function has_plugins_active() {
		return PluginsHelper::is_plugin_active( 'woocommerce-services' ) &&
		PluginsHelper::is_plugin_active( 'jetpack' );
	}

	/**
	 * Check if the Jetpack is connected.
	 *
	 * @return bool
	 */
	public static function has_jetpack_connected() {
		if ( class_exists( '\Jetpack' ) && is_callable( '\Jetpack::is_connection_ready' ) ) {
			return \Jetpack::is_connection_ready();
		}
		return false;
	}
}
OnboardingTasks/Tasks/GetMobileApp.php000064400000005014151542725270013754 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\Jetpack\Connection\Manager; // https://github.com/Automattic/jetpack/blob/trunk/projects/packages/connection/src/class-manager.php .

/**
 * Get Mobile App Task
 */
class GetMobileApp extends Task {
	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'get-mobile-app';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		return __( 'Get the free WooCommerce mobile app', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return '';
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return '';
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return get_option( 'woocommerce_admin_dismissed_mobile_app_modal' ) === 'yes';
	}

	/**
	 * Task visibility.
	 * Can view under these conditions:
	 *  - Jetpack is installed and connected && current site user has a wordpress.com account connected to jetpack
	 *  - Jetpack is not connected && current user is capable of installing plugins
	 *
	 * @return bool
	 */
	public function can_view() {
		$jetpack_can_be_installed                        = current_user_can( 'manage_woocommerce' ) && current_user_can( 'install_plugins' ) && ! self::is_jetpack_connected();
		$jetpack_is_installed_and_current_user_connected = self::is_current_user_connected();

		return $jetpack_can_be_installed || $jetpack_is_installed_and_current_user_connected;
	}

	/**
	 * Determines if site has any users connected to WordPress.com via JetPack
	 *
	 * @return bool
	 */
	private static function is_jetpack_connected() {
		if ( class_exists( '\Automattic\Jetpack\Connection\Manager' ) && method_exists( '\Automattic\Jetpack\Connection\Manager', 'is_active' ) ) {
			$connection = new Manager();
			return $connection->is_active();
		}
		return false;
	}

	/**
	 * Determines if the current user is connected to Jetpack.
	 *
	 * @return bool
	 */
	private static function is_current_user_connected() {
		if ( class_exists( '\Automattic\Jetpack\Connection\Manager' ) && method_exists( '\Automattic\Jetpack\Connection\Manager', 'is_user_connected' ) ) {
			$connection = new Manager();
			return $connection->is_connection_owner();
		}
		return false;
	}

	/**
	 * Action URL.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return admin_url( 'admin.php?page=wc-admin&mobileAppModal=true' );
	}
}
OnboardingTasks/Tasks/Marketing.php000064400000004736151542725270013377 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions\Init as RemoteFreeExtensions;

/**
 * Marketing Task
 */
class Marketing extends Task {
	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'marketing';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		if ( true === $this->get_parent_option( 'use_completed_title' ) ) {
			if ( $this->is_complete() ) {
				return __( 'You added sales channels', 'woocommerce' );
			}
			return __( 'Get more sales', 'woocommerce' );
		}
		return __( 'Set up marketing tools', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			'Add recommended marketing tools to reach new customers and grow your business',
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '2 minutes', 'woocommerce' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return self::has_installed_extensions();
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		return Features::is_enabled( 'remote-free-extensions' ) && count( self::get_plugins() ) > 0;
	}

	/**
	 * Get the marketing plugins.
	 *
	 * @return array
	 */
	public static function get_plugins() {
		$bundles = RemoteFreeExtensions::get_extensions(
			array(
				'task-list/reach',
				'task-list/grow',
			)
		);

		return array_reduce(
			$bundles,
			function( $plugins, $bundle ) {
				$visible = array();
				foreach ( $bundle['plugins'] as $plugin ) {
					if ( $plugin->is_visible ) {
						$visible[] = $plugin;
					}
				}
				return array_merge( $plugins, $visible );
			},
			array()
		);
	}

	/**
	 * Check if the store has installed marketing extensions.
	 *
	 * @return bool
	 */
	public static function has_installed_extensions() {
		$plugins   = self::get_plugins();
		$remaining = array();
		$installed = array();

		foreach ( $plugins as $plugin ) {
			if ( ! $plugin->is_installed ) {
				$remaining[] = $plugin;
			} else {
				$installed[] = $plugin;
			}
		}

		// Make sure the task has been actioned and a marketing extension has been installed.
		if ( count( $installed ) > 0 && Task::is_task_actioned( 'marketing' ) ) {
			return true;
		}

		return false;
	}
}
OnboardingTasks/Tasks/Payments.php000064400000003746151542725270013256 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;

/**
 * Payments Task
 */
class Payments extends Task {

	/**
	 * Used to cache is_complete() method result.
	 * @var null
	 */
	private $is_complete_result = null;

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'payments';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		if ( true === $this->get_parent_option( 'use_completed_title' ) ) {
			if ( $this->is_complete() ) {
				return __( 'You set up payments', 'woocommerce' );
			}
			return __( 'Set up payments', 'woocommerce' );
		}
		return __( 'Set up payments', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			'Choose payment providers and enable payment methods at checkout.',
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '2 minutes', 'woocommerce' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		if ( $this->is_complete_result === null ) {
			$this->is_complete_result = self::has_gateways();
		}

		return $this->is_complete_result;
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		$woocommerce_payments = $this->task_list->get_task( 'woocommerce-payments' );
		return Features::is_enabled( 'payment-gateway-suggestions' ) && ! $woocommerce_payments->can_view();
	}

	/**
	 * Check if the store has any enabled gateways.
	 *
	 * @return bool
	 */
	public static function has_gateways() {
		$gateways         = WC()->payment_gateways->get_available_payment_gateways();
		$enabled_gateways = array_filter(
			$gateways,
			function( $gateway ) {
				return 'yes' === $gateway->enabled && 'woocommerce_payments' !== $gateway->id;
			}
		);

		return ! empty( $enabled_gateways );
	}
}
OnboardingTasks/Tasks/Products.php000064400000007634151542725270013261 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;

/**
 * Products Task
 */
class Products extends Task {

	/**
	 * Constructor
	 *
	 * @param TaskList $task_list Parent task list.
	 */
	public function __construct( $task_list ) {
		parent::__construct( $task_list );
		add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_manual_return_notice_script' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_import_return_notice_script' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_load_sample_return_notice_script' ) );
	}

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'products';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		if ( $this->get_parent_option( 'use_completed_title' ) === true ) {
			if ( $this->is_complete() ) {
				return __( 'You added products', 'woocommerce' );
			}
			return __( 'Add products', 'woocommerce' );
		}
		return __( 'Add my products', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			'Start by adding the first product to your store. You can add your products manually, via CSV, or import them from another service.',
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '1 minute per product', 'woocommerce' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return self::has_products();
	}

	/**
	 * Addtional data.
	 *
	 * @return array
	 */
	public function get_additional_data() {
		return array(
			'has_products' => self::has_products(),
		);
	}


	/**
	 * Adds a return to task list notice when completing the manual product task.
	 *
	 * @param string $hook Page hook.
	 */
	public function possibly_add_manual_return_notice_script( $hook ) {
		global $post;
		if ( $hook !== 'post.php' || $post->post_type !== 'product' ) {
			return;
		}

		if ( ! $this->is_active() || ! $this->is_complete() ) {
			return;
		}

		WCAdminAssets::register_script( 'wp-admin-scripts', 'onboarding-product-notice', true );

		// Clear the active task transient to only show notice once per active session.
		delete_transient( self::ACTIVE_TASK_TRANSIENT );
	}

	/**
	 * Adds a return to task list notice when completing the import product task.
	 *
	 * @param string $hook Page hook.
	 */
	public function possibly_add_import_return_notice_script( $hook ) {
		$step = isset( $_GET['step'] ) ? $_GET['step'] : ''; // phpcs:ignore csrf ok, sanitization ok.

		if ( $hook !== 'product_page_product_importer' || $step !== 'done' ) {
			return;
		}

		if ( ! $this->is_active() || $this->is_complete() ) {
			return;
		}

		WCAdminAssets::register_script( 'wp-admin-scripts', 'onboarding-product-import-notice', true );
	}

	/**
	 * Adds a return to task list notice when completing the loading sample products action.
	 *
	 * @param string $hook Page hook.
	 */
	public function possibly_add_load_sample_return_notice_script( $hook ) {
		if ( $hook !== 'edit.php' || get_query_var( 'post_type' ) !== 'product' ) {
			return;
		}

		$referer = wp_get_referer();
		if ( ! $referer || strpos( $referer, wc_admin_url() ) !== 0 ) {
			return;
		}

		if ( ! isset( $_GET[ Task::ACTIVE_TASK_TRANSIENT ] ) ) {
			return;
		}

		$task_id = sanitize_title_with_dashes( wp_unslash( $_GET[ Task::ACTIVE_TASK_TRANSIENT ] ) );
		if ( $task_id !== $this->get_id() || ! $this->is_complete() ) {
			return;
		}

		WCAdminAssets::register_script( 'wp-admin-scripts', 'onboarding-load-sample-products-notice', true );
	}

	/**
	 * Check if the store has any published products.
	 *
	 * @return bool
	 */
	public static function has_products() {
		$counts = wp_count_posts('product');
		return isset( $counts->publish ) && $counts->publish > 0;
	}
}
OnboardingTasks/Tasks/Purchase.php000064400000012127151542725270013221 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProducts;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingThemes;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;

/**
 * Purchase Task
 */
class Purchase extends Task {
	/**
	 * Constructor
	 *
	 * @param TaskList $task_list Parent task list.
	 */
	public function __construct( $task_list ) {
		parent::__construct( $task_list );
		add_action( 'update_option_woocommerce_onboarding_profile', array( $this, 'clear_dismissal' ), 10, 2 );
	}

	/**
	 * Clear dismissal on onboarding product type changes.
	 *
	 * @param array $old_value Old value.
	 * @param array $new_value New value.
	 */
	public function clear_dismissal( $old_value, $new_value ) {
		$product_types          = isset( $new_value['product_types'] ) ? (array) $new_value['product_types'] : array();
		$previous_product_types = isset( $old_value['product_types'] ) ? (array) $old_value['product_types'] : array();

		if ( empty( array_diff( $product_types, $previous_product_types ) ) ) {
			return;
		}

		$this->undo_dismiss();
	}

	/**
	 * Get the task arguments.
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'purchase';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		$products   = $this->get_paid_products_and_themes();
		$first_product    = count( $products['purchaseable'] ) >= 1 ? $products['purchaseable'][0] : false;

		if ( ! $first_product ) {
			return null;
		}

		$product_label    = isset( $first_product['label'] ) ? $first_product['label'] : $first_product['title'];
		$additional_count = count( $products['purchaseable'] ) - 1;

		if ( $this->get_parent_option( 'use_completed_title' ) && $this->is_complete() ) {
			return count( $products['purchaseable'] ) === 1
				? sprintf(
					/* translators: %1$s: a purchased product name */
					__(
						'You added %1$s',
						'woocommerce'
					),
					$product_label
				)
				: sprintf(
					/* translators: %1$s: a purchased product name, %2$d the number of other products purchased */
					_n(
						'You added %1$s and %2$d other product',
						'You added %1$s and %2$d other products',
						$additional_count,
						'woocommerce'
					),
					$product_label,
					$additional_count
				);
		}

		return count( $products['purchaseable'] ) === 1
			? sprintf(
				/* translators: %1$s: a purchaseable product name */
				__(
					'Add %s to my store',
					'woocommerce'
				),
				$product_label
			)
			: sprintf(
				/* translators: %1$s: a purchaseable product name, %2$d the number of other products to purchase */
				_n(
					'Add %1$s and %2$d more product to my store',
					'Add %1$s and %2$d more products to my store',
					$additional_count,
					'woocommerce'
				),
				$product_label,
				$additional_count
			);
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		$products = $this->get_paid_products_and_themes();

		if ( count( $products['remaining'] ) === 1 ) {
			return isset( $products['purchaseable'][0]['description'] ) ? $products['purchaseable'][0]['description'] : $products['purchaseable'][0]['excerpt'];
		}
		return sprintf(
		/* translators: %1$s: list of product names comma separated, %2%s the last product name */
			__(
				'Good choice! You chose to add %1$s and %2$s to your store.',
				'woocommerce'
			),
			implode( ', ', array_slice( $products['remaining'], 0, -1 ) ) . ( count( $products['remaining'] ) > 2 ? ',' : '' ),
			end( $products['remaining'] )
		);
	}

	/**
	 * Action label.
	 *
	 * @return string
	 */
	public function get_action_label() {
		return __( 'Purchase & install now', 'woocommerce' );
	}


	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '2 minutes', 'woocommerce' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		$products = $this->get_paid_products_and_themes();
		return count( $products['remaining'] ) === 0;
	}

	/**
	 * Dismissable.
	 *
	 * @return bool
	 */
	public function is_dismissable() {
		return true;
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		$products = $this->get_paid_products_and_themes();
		return count( $products['purchaseable'] ) > 0;
	}

	/**
	 * Get purchaseable and remaining products.
	 *
	 * @return array purchaseable and remaining products and themes.
	 */
	public static function get_paid_products_and_themes() {
		$relevant_products = OnboardingProducts::get_relevant_products();

		$profiler_data = get_option( OnboardingProfile::DATA_OPTION, array() );
		$theme         = isset( $profiler_data['theme'] ) ? $profiler_data['theme'] : null;
		$paid_theme    = $theme ? OnboardingThemes::get_paid_theme_by_slug( $theme ) : null;
		if ( $paid_theme ) {

			$relevant_products['purchaseable'][] = $paid_theme;

			if ( isset( $paid_theme['is_installed'] ) && false === $paid_theme['is_installed'] ) {
				$relevant_products['remaining'][] = $paid_theme['title'];
			}
		}
		return $relevant_products;
	}
}
OnboardingTasks/Tasks/ReviewShippingOptions.php000064400000002177151542725270015772 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;

/**
 * Review Shipping Options Task
 */
class ReviewShippingOptions extends Task {
	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'review-shipping';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		return __( 'Review shipping options', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return '';
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return '';
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return get_option( 'woocommerce_admin_reviewed_default_shipping_zones' ) === 'yes';
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		return get_option( 'woocommerce_admin_created_default_shipping_zones' ) === 'yes';
	}

	/**
	 * Action URL.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return admin_url( 'admin.php?page=wc-settings&tab=shipping' );
	}
}
OnboardingTasks/Tasks/Shipping.php000064400000012244151542725270013230 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use WC_Data_Store;

/**
 * Shipping Task
 */
class Shipping extends Task {

	const ZONE_COUNT_TRANSIENT_NAME = 'woocommerce_shipping_task_zone_count_transient';

	/**
	 * Constructor
	 *
	 * @param TaskList $task_list Parent task list.
	 */
	public function __construct( $task_list = null ) {
		parent::__construct( $task_list );
		// wp_ajax_woocommerce_shipping_zone_methods_save_changes
		// and wp_ajax_woocommerce_shipping_zones_save_changes get fired
		// when a new zone is added or an existing one has been changed.
		add_action( 'wp_ajax_woocommerce_shipping_zones_save_changes', array( __CLASS__, 'delete_zone_count_transient' ), 9 );
		add_action( 'wp_ajax_woocommerce_shipping_zone_methods_save_changes', array( __CLASS__, 'delete_zone_count_transient' ), 9 );
		add_action( 'woocommerce_shipping_zone_method_added', array( __CLASS__, 'delete_zone_count_transient' ), 9 );
		add_action( 'woocommerce_after_shipping_zone_object_save', array( __CLASS__, 'delete_zone_count_transient' ), 9 );
	}

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'shipping';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		if ( true === $this->get_parent_option( 'use_completed_title' ) ) {
			if ( $this->is_complete() ) {
				return __( 'You added shipping costs', 'woocommerce' );
			}
			return __( 'Add shipping costs', 'woocommerce' );
		}
		return __( 'Set up shipping', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			"Set your store location and where you'll ship to.",
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '1 minute', 'woocommerce' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return self::has_shipping_zones();
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		if ( Features::is_enabled( 'shipping-smart-defaults' ) ) {
			if ( 'yes' === get_option( 'woocommerce_admin_created_default_shipping_zones' ) ) {
				// If the user has already created a default shipping zone, we don't need to show the task.
				return false;
			}

			/**
			 * Do not display the task when:
			 * - The store sells digital products only
			 * Display the task when:
			 * - We don't know where the store's located
			 * - The store is located in the UK, Australia or Canada
			*/

			if ( self::is_selling_digital_type_only() ) {
				return false;
			}

			$default_store_country = wc_format_country_state_string( get_option( 'woocommerce_default_country', '' ) )['country'];

			// Check if a store address is set so that we don't default to WooCommerce's default country US.
			// Similar logic: https://github.com/woocommerce/woocommerce/blob/059d542394b48468587f252dcb6941c6425cd8d3/plugins/woocommerce-admin/client/profile-wizard/steps/store-details/index.js#L511-L516.
			$store_country = '';
			if ( ! empty( get_option( 'woocommerce_store_address', '' ) ) || 'US' !== $default_store_country ) {
				$store_country = $default_store_country;
			}

			// Unknown country.
			if ( empty( $store_country ) ) {
				return true;
			}

			return in_array( $store_country, array( 'CA', 'AU', 'GB', 'ES', 'IT', 'DE', 'FR', 'MX', 'CO', 'CL', 'AR', 'PE', 'BR', 'UY', 'GT', 'NL', 'AT', 'BE' ), true );
		}

		return self::has_physical_products();
	}

	/**
	 * Action URL.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return self::has_shipping_zones()
			? admin_url( 'admin.php?page=wc-settings&tab=shipping' )
			: null;
	}

	/**
	 * Check if the store has any shipping zones.
	 *
	 * @return bool
	 */
	public static function has_shipping_zones() {
		$zone_count = get_transient( self::ZONE_COUNT_TRANSIENT_NAME );
		if ( false !== $zone_count ) {
			return (int) $zone_count > 0;
		}

		$zone_count = count( WC_Data_Store::load( 'shipping-zone' )->get_zones() );
		set_transient( self::ZONE_COUNT_TRANSIENT_NAME, $zone_count );

		return $zone_count > 0;
	}

	/**
	 * Check if the store has physical products.
	 *
	 * @return bool
	 */
	public static function has_physical_products() {
		$profiler_data = get_option( OnboardingProfile::DATA_OPTION, array() );
		$product_types = isset( $profiler_data['product_types'] ) ? $profiler_data['product_types'] : array();

		return in_array( 'physical', $product_types, true );
	}

	/**
	 * Delete the zone count transient used in has_shipping_zones() method
	 * to refresh the cache.
	 */
	public static function delete_zone_count_transient() {
		delete_transient( self::ZONE_COUNT_TRANSIENT_NAME );
	}

	/**
	 * Check if the store sells digital products only.
	 *
	 * @return bool
	 */
	private static function is_selling_digital_type_only() {
		$profiler_data = get_option( OnboardingProfile::DATA_OPTION, array() );
		$product_types = isset( $profiler_data['product_types'] ) ? $profiler_data['product_types'] : array();

		return array( 'downloads' ) === $product_types;
	}
}
OnboardingTasks/Tasks/StoreCreation.php000064400000002044151542725270014225 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\Onboarding;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;

/**
 * Store Details Task
 */
class StoreCreation extends Task {

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'store_creation';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		/* translators: Store name */
		return sprintf( __( 'You created %s', 'woocommerce' ), get_bloginfo( 'name' ) );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return '';
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return '';
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return '';
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return true;
	}

	/**
	 * Check if task is disabled.
	 *
	 * @return bool
	 */
	public function is_disabled() {
		return true;
	}
}

OnboardingTasks/Tasks/StoreDetails.php000064400000004207151542725270014051 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;

/**
 * Store Details Task
 */
class StoreDetails extends Task {
	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'store_details';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		if ( true === $this->get_parent_option( 'use_completed_title' ) ) {
			if ( $this->is_complete() ) {
				return __( 'You added store details', 'woocommerce' );
			}
			return __( 'Add store details', 'woocommerce' );
		}
		return __( 'Store details', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			'Your store address is required to set the origin country for shipping, currencies, and payment options.',
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '4 minutes', 'woocommerce' );
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return ! $this->is_complete() ? admin_url( 'admin.php?page=wc-settings&tab=general&tutorial=true' ) : admin_url( 'admin.php?page=wc-settings&tab=general' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		$country        = WC()->countries->get_base_country();
		$country_locale = WC()->countries->get_country_locale();
		$locale         = $country_locale[ $country ] ?? array();

		$hide_postcode = $locale['postcode']['hidden'] ?? false;
		// If postcode is hidden, just check that the store address and city are set.
		if ( $hide_postcode ) {
			return get_option( 'woocommerce_store_address', '' ) !== '' && get_option( 'woocommerce_store_city', '' ) !== '';
		}

		// Mark as completed if the store address, city and postcode are set. We don't need to check the country because it's set by default.
		return get_option( 'woocommerce_store_address', '' ) !== '' && get_option( 'woocommerce_store_city', '' ) !== '' &&
		get_option( 'woocommerce_store_postcode', '' ) !== '';
	}
}
OnboardingTasks/Tasks/Tax.php000064400000010045151542725270012200 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats\DataStore as TaxDataStore;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Admin\PluginsHelper;
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;

/**
 * Tax Task
 */
class Tax extends Task {

	/**
	 * Used to cache is_complete() method result.
	 * @var null
	 */
	private $is_complete_result = null;

	/**
	 * Constructor
	 *
	 * @param TaskList $task_list Parent task list.
	 */
	public function __construct( $task_list ) {
		parent::__construct( $task_list );
		add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_return_notice_script' ) );
	}

	/**
	 * Adds a return to task list notice when completing the task.
	 */
	public function possibly_add_return_notice_script() {
		$page = isset( $_GET['page'] ) ? $_GET['page'] : ''; // phpcs:ignore csrf ok, sanitization ok.
		$tab  = isset( $_GET['tab'] ) ? $_GET['tab'] : ''; // phpcs:ignore csrf ok, sanitization ok.

		if ( $page !== 'wc-settings' || $tab !== 'tax' ) {
			return;
		}

		if ( ! $this->is_active() || $this->is_complete() ) {
			return;
		}

		WCAdminAssets::register_script( 'wp-admin-scripts', 'onboarding-tax-notice', true );
	}

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'tax';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		if ( $this->get_parent_option( 'use_completed_title' ) === true ) {
			if ( $this->is_complete() ) {
				return __( 'You added tax rates', 'woocommerce' );
			}
			return __( 'Add tax rates', 'woocommerce' );
		}
		return __( 'Set up tax rates', 'woocommerce' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return self::can_use_automated_taxes()
			? __(
				'Good news! WooCommerce Services and Jetpack can automate your sales tax calculations for you.',
				'woocommerce'
			)
			: __(
				'Set your store location and configure tax rate settings.',
				'woocommerce'
			);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '1 minute', 'woocommerce' );
	}

	/**
	 * Action label.
	 *
	 * @return string
	 */
	public function get_action_label() {
		return self::can_use_automated_taxes()
			? __( 'Yes please', 'woocommerce' )
			: __( "Let's go", 'woocommerce' );
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		if ( $this->is_complete_result === null ) {
			$wc_connect_taxes_enabled = get_option( 'wc_connect_taxes_enabled' );
			$is_wc_connect_taxes_enabled = ( $wc_connect_taxes_enabled === 'yes' ) || ( $wc_connect_taxes_enabled === true ); // seems that in some places boolean is used, and other places 'yes' | 'no' is used

			$this->is_complete_result = $is_wc_connect_taxes_enabled ||
				count( TaxDataStore::get_taxes( array() ) ) > 0 ||
				get_option( 'woocommerce_no_sales_tax' ) !== false;
		}

		return $this->is_complete_result;
	}

	/**
	 * Addtional data.
	 *
	 * @return array
	 */
	public function get_additional_data() {
		return array(
			'avalara_activated'         => PluginsHelper::is_plugin_active( 'woocommerce-avatax' ),
			'tax_jar_activated'         => class_exists( 'WC_Taxjar' ),
			'woocommerce_tax_countries' => self::get_automated_support_countries(),
		);
	}

	/**
	 * Check if the store has any enabled gateways.
	 *
	 * @return bool
	 */
	public static function can_use_automated_taxes() {
		if ( ! class_exists( 'WC_Taxjar' ) ) {
			return false;
		}

		return in_array( WC()->countries->get_base_country(), self::get_automated_support_countries(), true );
	}

	/**
	 * Get an array of countries that support automated tax.
	 *
	 * @return array
	 */
	public static function get_automated_support_countries() {
		// https://developers.taxjar.com/api/reference/#countries .
		$tax_supported_countries = array_merge(
			array( 'US', 'CA', 'AU', 'GB' ),
			WC()->countries->get_european_union_countries()
		);

		return $tax_supported_countries;
	}
}
OnboardingTasks/Tasks/TourInAppMarketplace.php000064400000002277151542725270015506 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;

/**
 * Tour In-App Marketplace task
 */
class TourInAppMarketplace extends Task {
	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'tour-in-app-marketplace';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		return __(
			'Discover ways of extending your store with a tour of the Woo Marketplace',
			'woocommerce'
		);
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return '';
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return '';
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		return get_option( 'woocommerce_admin_dismissed_in_app_marketplace_tour' ) === 'yes';
	}

	/**
	 * Action URL.
	 *
	 * @return string
	 */
	public function get_action_url() {
		return admin_url( 'admin.php?page=wc-admin&path=%2Fextensions&tutorial=true' );
	}

	/**
	 * Check if should record event when task is viewed
	 *
	 * @return bool
	 */
	public function get_record_view_event(): bool {
		return true;
	}
}
OnboardingTasks/Tasks/WooCommercePayments.php000064400000011433151542725270015406 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;

use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Admin\PluginsHelper;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\Init as Suggestions;
use Automattic\WooCommerce\Internal\Admin\WCPayPromotion\Init as WCPayPromotionInit;

/**
 * WooCommercePayments Task
 */
class WooCommercePayments extends Task {
	/**
	 * Used to cache is_complete() method result.
	 *
	 * @var null
	 */
	private $is_complete_result = null;

	/**
	 * ID.
	 *
	 * @return string
	 */
	public function get_id() {
		return 'woocommerce-payments';
	}

	/**
	 * Title.
	 *
	 * @return string
	 */
	public function get_title() {
		return __( 'Set up WooPayments', 'woocommerce' );
	}

	/**
	 * Badge.
	 *
	 * @return string
	 */
	public function get_badge() {
		/**
		 * Filter WooPayments onboarding task badge.
		 *
		 * @param string     $badge    Badge content.
		 * @since 8.2.0
		 */
		return apply_filters( 'woocommerce_admin_woopayments_onboarding_task_badge', '' );
	}

	/**
	 * Content.
	 *
	 * @return string
	 */
	public function get_content() {
		return __(
			"You're only one step away from getting paid. Verify your business details to start managing transactions with WooPayments.",
			'woocommerce'
		);
	}

	/**
	 * Time.
	 *
	 * @return string
	 */
	public function get_time() {
		return __( '2 minutes', 'woocommerce' );
	}

	/**
	 * Action label.
	 *
	 * @return string
	 */
	public function get_action_label() {
		return __( 'Finish setup', 'woocommerce' );
	}

	/**
	 * Additional info.
	 *
	 * @return string
	 */
	public function get_additional_info() {
		if ( WCPayPromotionInit::is_woopay_eligible() ) {
			return __(
				'By using WooPayments you agree to be bound by our <a href="https://wordpress.com/tos/" target="_blank">Terms of Service</a> (including WooPay <a href="https://wordpress.com/tos/#more-woopay-specifically" target="_blank">merchant terms</a>) and acknowledge that you have read our <a href="https://automattic.com/privacy/" target="_blank">Privacy Policy</a>',
				'woocommerce'
			);
		}

		return __(
			'By using WooPayments you agree to be bound by our <a href="https://wordpress.com/tos/" target="_blank">Terms of Service</a> and acknowledge that you have read our <a href="https://automattic.com/privacy/" target="_blank">Privacy Policy</a>',
			'woocommerce'
		);
	}

	/**
	 * Task completion.
	 *
	 * @return bool
	 */
	public function is_complete() {
		if ( null === $this->is_complete_result ) {
			$this->is_complete_result = self::is_connected();
		}

		return $this->is_complete_result;
	}

	/**
	 * Task visibility.
	 *
	 * @return bool
	 */
	public function can_view() {
		$payments = $this->task_list->get_task( 'payments' );

		return ! $payments->is_complete() && // Do not re-display the task if the "add payments" task has already been completed.
			self::is_installed() &&
			self::is_supported();
	}

	/**
	 * Check if the plugin was requested during onboarding.
	 *
	 * @return bool
	 */
	public static function is_requested() {
		$profiler_data       = get_option( OnboardingProfile::DATA_OPTION, array() );
		$product_types       = isset( $profiler_data['product_types'] ) ? $profiler_data['product_types'] : array();
		$business_extensions = isset( $profiler_data['business_extensions'] ) ? $profiler_data['business_extensions'] : array();

		$subscriptions_and_us = in_array( 'subscriptions', $product_types, true ) && 'US' === WC()->countries->get_base_country();
		return in_array( 'woocommerce-payments', $business_extensions, true ) || $subscriptions_and_us;
	}

	/**
	 * Check if the plugin is installed.
	 *
	 * @return bool
	 */
	public static function is_installed() {
		$installed_plugins = PluginsHelper::get_installed_plugin_slugs();
		return in_array( 'woocommerce-payments', $installed_plugins, true );
	}

	/**
	 * Check if WooCommerce Payments is connected.
	 *
	 * @return bool
	 */
	public static function is_connected() {
		if ( class_exists( '\WC_Payments' ) ) {
			$wc_payments_gateway = \WC_Payments::get_gateway();
			return method_exists( $wc_payments_gateway, 'is_connected' )
				? $wc_payments_gateway->is_connected()
				: false;
		}

		return false;
	}

	/**
	 * Check if the store is in a supported country.
	 *
	 * @return bool
	 */
	public static function is_supported() {
		$suggestions              = Suggestions::get_suggestions();
		$suggestion_plugins       = array_merge(
			...array_filter(
				array_column( $suggestions, 'plugins' ),
				function( $plugins ) {
					return is_array( $plugins );
				}
			)
		);
		$woocommerce_payments_ids = array_search( 'woocommerce-payments', $suggestion_plugins, true );
		if ( false !== $woocommerce_payments_ids ) {
			return true;
		}
		return false;
	}
}
PaymentGatewaySuggestions/DefaultPaymentGateways.php000064400000106474151542725270017124 0ustar00<?php
/**
 * Gets a list of fallback methods if remote fetching is disabled.
 */

namespace Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions;

defined( 'ABSPATH' ) || exit;

/**
 * Default Payment Gateways
 */
class DefaultPaymentGateways {
	/**
	 * This is the default priority for countries that are not in the $recommendation_priority_map.
	 * Priority is used to determine which payment gateway to recommend first.
	 * The lower the number, the higher the priority.
	 *
	 * @var array
	 */
	private static $recommendation_priority = array(
		'woocommerce_payments'                            => 1,
		'woocommerce_payments:with-in-person-payments'    => 1,
		'woocommerce_payments:without-in-person-payments' => 1,
		'stripe'                                          => 2,
		'woo-mercado-pago-custom'                         => 3,
		// PayPal Payments.
		'ppcp-gateway'                                    => 4,
		'mollie_wc_gateway_banktransfer'                  => 5,
		'razorpay'                                        => 5,
		'payfast'                                         => 5,
		'payubiz'                                         => 6,
		'square_credit_card'                              => 6,
		'klarna_payments'                                 => 6,
		// Klarna Checkout.
		'kco'                                             => 6,
		'paystack'                                        => 6,
		'eway'                                            => 7,
		'amazon_payments_advanced'                        => 7,
		'affirm'                                          => 8,
		'afterpay'                                        => 9,
		'zipmoney'                                        => 10,
		'payoneer-checkout'                               => 11,
	);

	/**
	 * Get default specs.
	 *
	 * @return array Default specs.
	 */
	public static function get_all() {
		$payment_gateways = array(
			array(
				'id'                  => 'affirm',
				'title'               => __( 'Affirm', 'woocommerce' ),
				'content'             => __( 'Affirm’s tailored Buy Now Pay Later programs remove price as a barrier, turning browsers into buyers, increasing average order value, and expanding your customer base.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/affirm.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/affirm.png',
				'plugins'             => array(),
				'external_link'       => 'https://woocommerce.com/products/woocommerce-gateway-affirm',
				'is_visible'          => array(
					self::get_rules_for_countries(
						array(
							'US',
							'CA',
						)
					),
				),
				'category_other'      => array(),
				'category_additional' => array(
					'US',
					'CA',
				),
			),
			array(
				'id'                  => 'afterpay',
				'title'               => __( 'Afterpay', 'woocommerce' ),
				'content'             => __( 'Afterpay allows customers to receive products immediately and pay for purchases over four installments, always interest-free.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/afterpay.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/afterpay.png',
				'plugins'             => array( 'afterpay-gateway-for-woocommerce' ),
				'is_visible'          => array(
					self::get_rules_for_countries(
						array(
							'US',
							'CA',
							'AU',
						)
					),
				),
				'category_other'      => array(),
				'category_additional' => array(
					'US',
					'CA',
					'AU',
				),
			),
			array(
				'id'                  => 'amazon_payments_advanced',
				'title'               => __( 'Amazon Pay', 'woocommerce' ),
				'content'             => __( 'Enable a familiar, fast checkout for hundreds of millions of active Amazon customers globally.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/amazonpay.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/amazonpay.png',
				'plugins'             => array( 'woocommerce-gateway-amazon-payments-advanced' ),
				'is_visible'          => array(
					self::get_rules_for_countries(
						array(
							'US',
							'AT',
							'BE',
							'CY',
							'DK',
							'ES',
							'FR',
							'DE',
							'GB',
							'HU',
							'IE',
							'IT',
							'LU',
							'NL',
							'PT',
							'SL',
							'SE',
							'JP',
						)
					),
				),
				'category_other'      => array(),
				'category_additional' => array(
					'US',
					'AT',
					'BE',
					'CY',
					'DK',
					'ES',
					'FR',
					'DE',
					'GB',
					'HU',
					'IE',
					'IT',
					'LU',
					'NL',
					'PT',
					'SL',
					'SE',
					'JP',
				),
			),
			array(
				'id'          => 'bacs',
				'title'       => __( 'Direct bank transfer', 'woocommerce' ),
				'content'     => __( 'Take payments via bank transfer.', 'woocommerce' ),
				'image'       => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/bacs.svg',
				'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/bacs.png',
				'is_visible'  => array(
					self::get_rules_for_cbd( false ),
				),
				'is_offline'  => true,
			),
			array(
				'id'          => 'cod',
				'title'       => __( 'Cash on delivery', 'woocommerce' ),
				'content'     => __( 'Take payments in cash upon delivery.', 'woocommerce' ),
				'image'       => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/cod.svg',
				'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/cod.png',
				'is_visible'  => array(
					self::get_rules_for_cbd( false ),
				),
				'is_offline'  => true,
			),
			array(
				'id'                  => 'eway',
				'title'               => __( 'Eway', 'woocommerce' ),
				'content'             => __( 'The Eway extension for WooCommerce allows you to take credit card payments directly on your store without redirecting your customers to a third party site to make payment.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/eway.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/eway.png',
				'plugins'             => array( 'woocommerce-gateway-eway' ),
				'is_visible'          => array(
					self::get_rules_for_countries(
						array(
							'NZ',
							'HK',
							'SG',
							'AU',
						)
					),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array(
					'NZ',
					'HK',
					'SG',
					'AU',
				),
				'category_additional' => array(),
			),
			array(
				'id'                  => 'kco',
				'title'               => __( 'Klarna Checkout', 'woocommerce' ),
				'content'             => __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/klarna-black.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/klarna.png',
				'plugins'             => array( 'klarna-checkout-for-woocommerce' ),
				'is_visible'          => array(
					self::get_rules_for_countries(
						array(
							'NO',
							'SE',
							'FI',
						)
					),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array(
					'NO',
					'SE',
					'FI',
				),
				'category_additional' => array(),
			),
			array(
				'id'                  => 'klarna_payments',
				'title'               => __( 'Klarna Payments', 'woocommerce' ),
				'content'             => __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/klarna-black.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/klarna.png',
				'plugins'             => array( 'klarna-payments-for-woocommerce' ),
				'is_visible'          => array(
					self::get_rules_for_countries(
						array(
							'MX',
							'US',
							'CA',
							'AT',
							'BE',
							'CH',
							'DK',
							'ES',
							'FI',
							'FR',
							'DE',
							'GB',
							'IT',
							'NL',
							'NO',
							'PL',
							'SE',
							'NZ',
							'AU',
						)
					),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array(),
				'category_additional' => array(
					'MX',
					'US',
					'CA',
					'AT',
					'BE',
					'CH',
					'DK',
					'ES',
					'FI',
					'FR',
					'DE',
					'GB',
					'IT',
					'NL',
					'NO',
					'PL',
					'SE',
					'NZ',
					'AU',
				),
			),
			array(
				'id'                  => 'mollie_wc_gateway_banktransfer',
				'title'               => __( 'Mollie', 'woocommerce' ),
				'content'             => __( 'Effortless payments by Mollie: Offer global and local payment methods, get onboarded in minutes, and supported in your language.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/mollie.svg',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/mollie.png',
				'plugins'             => array( 'mollie-payments-for-woocommerce' ),
				'is_visible'          => array(
					self::get_rules_for_countries(
						array(
							'AT',
							'BE',
							'CH',
							'ES',
							'FI',
							'FR',
							'DE',
							'GB',
							'IT',
							'NL',
							'PL',
						)
					),
				),
				'category_other'      => array(
					'AT',
					'BE',
					'CH',
					'ES',
					'FI',
					'FR',
					'DE',
					'GB',
					'IT',
					'NL',
					'PL',
				),
				'category_additional' => array(),
			),
			array(
				'id'                  => 'payfast',
				'title'               => __( 'Payfast', 'woocommerce' ),
				'content'             => __( 'The Payfast extension for WooCommerce enables you to accept payments by Credit Card and EFT via one of South Africa’s most popular payment gateways. No setup fees or monthly subscription costs. Selecting this extension will configure your store to use South African rands as the selected currency.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/payfast.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/payfast.png',
				'plugins'             => array( 'woocommerce-payfast-gateway' ),
				'is_visible'          => array(
					self::get_rules_for_countries( array( 'ZA' ) ),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array( 'ZA' ),
				'category_additional' => array(),
			),
			array(
				'id'                  => 'payoneer-checkout',
				'title'               => __( 'Payoneer Checkout', 'woocommerce' ),
				'content'             => __( 'Payoneer Checkout is the next generation of payment processing platforms, giving merchants around the world the solutions and direction they need to succeed in today’s hyper-competitive global market.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/payoneer.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/payoneer.png',
				'plugins'             => array( 'payoneer-checkout' ),
				'is_visible'          => array(
					self::get_rules_for_countries(
						array(
							'HK',
							'CN',
						)
					),
				),
				'category_other'      => array(),
				'category_additional' => array(
					'HK',
					'CN',
				),
			),
			array(
				'id'                  => 'paystack',
				'title'               => __( 'Paystack', 'woocommerce' ),
				'content'             => __( 'Paystack helps African merchants accept one-time and recurring payments online with a modern, safe, and secure payment gateway.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/paystack.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/paystack.png',
				'plugins'             => array( 'woo-paystack' ),
				'is_visible'          => array(
					self::get_rules_for_countries( array( 'ZA', 'GH', 'NG' ) ),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array( 'ZA', 'GH', 'NG' ),
				'category_additional' => array(),
			),
			array(
				'id'                  => 'payubiz',
				'title'               => __( 'PayU for WooCommerce', 'woocommerce' ),
				'content'             => __( 'Enable PayU’s exclusive plugin for WooCommerce to start accepting payments in 100+ payment methods available in India including credit cards, debit cards, UPI, & more!', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/payu.svg',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/payu.png',
				'plugins'             => array( 'payu-india' ),
				'is_visible'          => array(
					(object) array(
						'type'      => 'base_location_country',
						'value'     => 'IN',
						'operation' => '=',
					),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array( 'IN' ),
				'category_additional' => array(),
			),
			array(
				'id'                  => 'ppcp-gateway',
				'title'               => __( 'PayPal Payments', 'woocommerce' ),
				'content'             => __( "Safe and secure payments using credit cards or your customer's PayPal account.", 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/paypal.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/paypal.png',
				'plugins'             => array( 'woocommerce-paypal-payments' ),
				'is_visible'          => array(
					self::get_rules_for_countries(
						array(
							'US',
							'CA',
							'MX',
							'BR',
							'AR',
							'CL',
							'CO',
							'EC',
							'PE',
							'UY',
							'VE',
							'AT',
							'BE',
							'BG',
							'HR',
							'CH',
							'CY',
							'CZ',
							'DK',
							'EE',
							'ES',
							'FI',
							'FR',
							'DE',
							'GB',
							'GR',
							'HU',
							'IE',
							'IT',
							'LV',
							'LT',
							'LU',
							'MT',
							'NL',
							'NO',
							'PL',
							'PT',
							'RO',
							'SK',
							'SL',
							'SE',
							'AU',
							'NZ',
							'HK',
							'JP',
							'SG',
							'CN',
							'ID',
							'IN',
						)
					),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array(
					'US',
					'CA',
					'MX',
					'BR',
					'AR',
					'CL',
					'CO',
					'EC',
					'PE',
					'UY',
					'VE',
					'AT',
					'BE',
					'BG',
					'HR',
					'CH',
					'CY',
					'CZ',
					'DK',
					'EE',
					'ES',
					'FI',
					'FR',
					'DE',
					'GB',
					'GR',
					'HU',
					'IE',
					'IT',
					'LV',
					'LT',
					'LU',
					'MT',
					'NL',
					'NO',
					'PL',
					'PT',
					'RO',
					'SK',
					'SL',
					'SE',
					'AU',
					'NZ',
					'HK',
					'JP',
					'SG',
					'CN',
					'ID',
				),
				'category_additional' => array(
					'US',
					'CA',
					'ZA',
					'NG',
					'GH',
					'EC',
					'VE',
					'AR',
					'CL',
					'CO',
					'PE',
					'UY',
					'MX',
					'BR',
					'AT',
					'BE',
					'BG',
					'HR',
					'CH',
					'CY',
					'CZ',
					'DK',
					'EE',
					'ES',
					'FI',
					'FR',
					'DE',
					'GB',
					'GR',
					'HU',
					'IE',
					'IT',
					'LV',
					'LT',
					'LU',
					'MT',
					'NL',
					'NO',
					'PL',
					'PT',
					'RO',
					'SK',
					'SL',
					'SE',
					'AU',
					'NZ',
					'HK',
					'JP',
					'SG',
					'CN',
					'ID',
					'IN',
				),
			),
			array(
				'id'                  => 'razorpay',
				'title'               => __( 'Razorpay', 'woocommerce' ),
				'content'             => __( 'The official Razorpay extension for WooCommerce allows you to accept credit cards, debit cards, netbanking, wallet, and UPI payments.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/razorpay.svg',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/razorpay.png',
				'plugins'             => array( 'woo-razorpay' ),
				'is_visible'          => array(
					(object) array(
						'type'      => 'base_location_country',
						'value'     => 'IN',
						'operation' => '=',
					),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array( 'IN' ),
				'category_additional' => array(),
			),
			array(
				'id'                  => 'square_credit_card',
				'title'               => __( 'Square', 'woocommerce' ),
				'content'             => __( 'Securely accept credit and debit cards with one low rate, no surprise fees (custom rates available). Sell online and in store and track sales and inventory in one place.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/square-black.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/square.png',
				'plugins'             => array( 'woocommerce-square' ),
				'is_visible'          => array(
					(object) array(
						'type'     => 'or',
						'operands' => (object) array(
							array(
								self::get_rules_for_countries( array( 'US' ) ),
								self::get_rules_for_cbd( true ),
							),
							array(
								self::get_rules_for_countries(
									array(
										'US',
										'CA',
										'IE',
										'ES',
										'FR',
										'GB',
										'AU',
										'JP',
									)
								),
								(object) array(
									'type'     => 'or',
									'operands' => (object) array(
										self::get_rules_for_selling_venues( array( 'brick-mortar', 'brick-mortar-other' ) ),
										self::get_rules_selling_offline(),
									),
								),
							),
						),
					),
				),
				'category_other'      => array(
					'US',
					'CA',
					'IE',
					'ES',
					'FR',
					'GB',
					'AU',
					'JP',
				),
				'category_additional' => array(),
			),
			array(
				'id'                  => 'stripe',
				'title'               => __( ' Stripe', 'woocommerce' ),
				'content'             => __( 'Accept debit and credit cards in 135+ currencies, methods such as Alipay, and one-touch checkout with Apple Pay.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/stripe.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/stripe.png',
				'plugins'             => array( 'woocommerce-gateway-stripe' ),
				'is_visible'          => array(
					// https://stripe.com/global.
					self::get_rules_for_countries(
						array(
							'US',
							'CA',
							'MX',
							'BR',
							'AT',
							'BE',
							'BG',
							'CH',
							'CY',
							'CZ',
							'DK',
							'EE',
							'ES',
							'FI',
							'FR',
							'DE',
							'GB',
							'GR',
							'HU',
							'IE',
							'IT',
							'LV',
							'LT',
							'LU',
							'MT',
							'NL',
							'NO',
							'PL',
							'PT',
							'RO',
							'SK',
							'SL',
							'SE',
							'AU',
							'NZ',
							'HK',
							'JP',
							'SG',
							'ID',
							'IN',
						)
					),
					self::get_rules_for_cbd( false ),
				),
				'category_other'      => array(
					'US',
					'CA',
					'MX',
					'BR',
					'AT',
					'BE',
					'BG',
					'CH',
					'CY',
					'CZ',
					'DK',
					'EE',
					'ES',
					'FI',
					'FR',
					'DE',
					'GB',
					'GR',
					'HU',
					'IE',
					'IT',
					'LV',
					'LT',
					'LU',
					'MT',
					'NL',
					'NO',
					'PL',
					'PT',
					'RO',
					'SK',
					'SL',
					'SE',
					'AU',
					'NZ',
					'HK',
					'JP',
					'SG',
					'ID',
					'IN',
				),
				'category_additional' => array(),
			),
			array(
				'id'                  => 'woo-mercado-pago-custom',
				'title'               => __( 'Mercado Pago Checkout Pro & Custom', 'woocommerce' ),
				'content'             => __( 'Accept credit and debit cards, offline (cash or bank transfer) and logged-in payments with money in Mercado Pago. Safe and secure payments with the leading payment processor in LATAM.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/mercadopago.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/mercadopago.png',
				'plugins'             => array( 'woocommerce-mercadopago' ),
				'is_visible'          => array(
					self::get_rules_for_countries(
						array(
							'AR',
							'CL',
							'CO',
							'EC',
							'PE',
							'UY',
							'MX',
							'BR',
						)
					),
				),
				'is_local_partner'    => true,
				'category_other'      => array(
					'AR',
					'CL',
					'CO',
					'EC',
					'PE',
					'UY',
					'MX',
					'BR',
				),
				'category_additional' => array(),
			),
			// This is for backwards compatibility only (WC < 5.10.0-dev or WCA < 2.9.0-dev).
			array(
				'id'          => 'woocommerce_payments',
				'title'       => __( 'WooPayments', 'woocommerce' ),
				'content'     => __(
					'Manage transactions without leaving your WordPress Dashboard. Only with WooPayments.',
					'woocommerce'
				),
				'image'       => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg',
				'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg',
				'plugins'     => array( 'woocommerce-payments' ),
				'description' => __( 'With WooPayments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies. Track cash flow and manage recurring revenue directly from your store’s dashboard - with no setup costs or monthly fees.', 'woocommerce' ),
				'is_visible'  => array(
					self::get_rules_for_cbd( false ),
					self::get_rules_for_countries( self::get_wcpay_countries() ),
					(object) array(
						'type'     => 'plugin_version',
						'plugin'   => 'woocommerce',
						'version'  => '5.10.0-dev',
						'operator' => '<',
					),
					(object) array(
						'type'     => 'or',
						'operands' => (object) array(
							(object) array(
								'type'    => 'not',
								'operand' => [
									(object) array(
										'type'    => 'plugins_activated',
										'plugins' => [ 'woocommerce-admin' ],
									),
								],
							),
							(object) array(
								'type'     => 'plugin_version',
								'plugin'   => 'woocommerce-admin',
								'version'  => '2.9.0-dev',
								'operator' => '<',
							),
						),
					),
				),
			),
			array(
				'id'          => 'woocommerce_payments:without-in-person-payments',
				'title'       => __( 'WooPayments', 'woocommerce' ),
				'content'     => __(
					'Manage transactions without leaving your WordPress Dashboard. Only with WooPayments.',
					'woocommerce'
				),
				'image'       => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg',
				'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg',
				'plugins'     => array( 'woocommerce-payments' ),
				'description' => __( 'With WooPayments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies. Track cash flow and manage recurring revenue directly from your store’s dashboard - with no setup costs or monthly fees.', 'woocommerce' ),
				'is_visible'  => array(
					self::get_rules_for_cbd( false ),
					self::get_rules_for_countries( array_diff( self::get_wcpay_countries(), array( 'US', 'CA' ) ) ),
					(object) array(
						'type'     => 'or',
						// Older versions of WooCommerce Admin require the ID to be `woocommerce-payments` to show the suggestion card.
						'operands' => (object) array(
							(object) array(
								'type'     => 'plugin_version',
								'plugin'   => 'woocommerce-admin',
								'version'  => '2.9.0-dev',
								'operator' => '>=',
							),
							(object) array(
								'type'     => 'plugin_version',
								'plugin'   => 'woocommerce',
								'version'  => '5.10.0-dev',
								'operator' => '>=',
							),
						),
					),
				),
			),
			// This is the same as the above, but with a different description for countries that support in-person payments such as US and CA.
			array(
				'id'          => 'woocommerce_payments:with-in-person-payments',
				'title'       => __( 'WooPayments', 'woocommerce' ),
				'content'     => __(
					'Manage transactions without leaving your WordPress Dashboard. Only with WooPayments.',
					'woocommerce'
				),
				'image'       => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg',
				'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay.svg',
				'plugins'     => array( 'woocommerce-payments' ),
				'description' => __( 'With WooPayments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies – with no setup costs or monthly fees – and you can now accept in-person payments with the Woo mobile app.', 'woocommerce' ),
				'is_visible'  => array(
					self::get_rules_for_cbd( false ),
					self::get_rules_for_countries( array( 'US', 'CA' ) ),
					(object) array(
						'type'     => 'or',
						// Older versions of WooCommerce Admin require the ID to be `woocommerce-payments` to show the suggestion card.
						'operands' => (object) array(
							(object) array(
								'type'     => 'plugin_version',
								'plugin'   => 'woocommerce-admin',
								'version'  => '2.9.0-dev',
								'operator' => '>=',
							),
							(object) array(
								'type'     => 'plugin_version',
								'plugin'   => 'woocommerce',
								'version'  => '5.10.0-dev',
								'operator' => '>=',
							),
						),
					),
				),
			),
			array(
				'id'                  => 'zipmoney',
				'title'               => __( 'Zip Co - Buy Now, Pay Later', 'woocommerce' ),
				'content'             => __( 'Give your customers the power to pay later, interest free and watch your sales grow.', 'woocommerce' ),
				'image'               => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/zipco.png',
				'image_72x72'         => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/zipco.png',
				'plugins'             => array( 'zipmoney-payments-woocommerce' ),
				'is_visible'          => array(
					self::get_rules_for_countries(
						array(
							'US',
							'NZ',
							'AU',
						)
					),
				),
				'category_other'      => array(),
				'category_additional' => array(
					'US',
					'NZ',
					'AU',
				),
			),
		);

		$base_location = wc_get_base_location();
		$country       = $base_location['country'];
		foreach ( $payment_gateways as $index => $payment_gateway ) {
			$payment_gateways[ $index ]['recommendation_priority'] = self::get_recommendation_priority( $payment_gateway['id'], $country );
		}

		return $payment_gateways;
	}

	/**
	 * Get array of countries supported by WCPay depending on feature flag.
	 *
	 * @return array Array of countries.
	 */
	public static function get_wcpay_countries() {
		return array( 'US', 'PR', 'AU', 'CA', 'CY', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'IE', 'IT', 'LU', 'LT', 'LV', 'NO', 'NZ', 'MT', 'AT', 'BE', 'NL', 'PL', 'PT', 'CH', 'HK', 'SI', 'SK', 'SG', 'BG', 'CZ', 'HR', 'HU', 'RO', 'SE', 'JP', 'AE' );
	}

	/**
	 * Get rules that match the store base location to one of the provided countries.
	 *
	 * @param array $countries Array of countries to match.
	 * @return object Rules to match.
	 */
	public static function get_rules_for_countries( $countries ) {
		$rules = array();

		foreach ( $countries as $country ) {
			$rules[] = (object) array(
				'type'      => 'base_location_country',
				'value'     => $country,
				'operation' => '=',
			);
		}

		return (object) array(
			'type'     => 'or',
			'operands' => $rules,
		);
	}

	/**
	 * Get rules that match the store's selling venues.
	 *
	 * @param array $selling_venues Array of venues to match.
	 * @return object Rules to match.
	 */
	public static function get_rules_for_selling_venues( $selling_venues ) {
		$rules = array();

		foreach ( $selling_venues as $venue ) {
			$rules[] = (object) array(
				'type'         => 'option',
				'transformers' => array(
					(object) array(
						'use'       => 'dot_notation',
						'arguments' => (object) array(
							'path' => 'selling_venues',
						),
					),
				),
				'option_name'  => 'woocommerce_onboarding_profile',
				'operation'    => '=',
				'value'        => $venue,
				'default'      => array(),
			);
		}

		return (object) array(
			'type'     => 'or',
			'operands' => $rules,
		);
	}

	/**
	 * Get rules for when selling offline for core profiler.
	 *
	 * @return object Rules to match.
	 */
	public static function get_rules_selling_offline() {
		return (object) array(
			'type'         => 'option',
			'transformers' => array(
				(object) array(
					'use'       => 'dot_notation',
					'arguments' => (object) array(
						'path' => 'selling_online_answer',
					),
				),
			),
			'option_name'  => 'woocommerce_onboarding_profile',
			'operation'    => 'contains',
			'value'        => 'no_im_selling_offline',
			'default'      => array(),
		);
	}

	/**
	 * Get default rules for CBD based on given argument.
	 *
	 * @param bool $should_have Whether or not the store should have CBD as an industry (true) or not (false).
	 * @return array Rules to match.
	 */
	public static function get_rules_for_cbd( $should_have ) {
		return (object) array(
			'type'         => 'option',
			'transformers' => array(
				(object) array(
					'use'       => 'dot_notation',
					'arguments' => (object) array(
						'path' => 'industry',
					),
				),
				(object) array(
					'use'       => 'array_column',
					'arguments' => (object) array(
						'key' => 'slug',
					),
				),
			),
			'option_name'  => 'woocommerce_onboarding_profile',
			'operation'    => $should_have ? 'contains' : '!contains',
			'value'        => 'cbd-other-hemp-derived-products',
			'default'      => array(),
		);
	}

	/**
	 * Get recommendation priority for a given payment gateway by id and country.
	 * If country is not supported, return null.
	 *
	 * @param string $gateway_id Payment gateway id.
	 * @param string $country_code Store country code.
	 * @return int|null Priority. Priority is 0-indexed, so 0 is the highest priority.
	 */
	private static function get_recommendation_priority( $gateway_id, $country_code ) {
		$recommendation_priority_map = array(
			'US' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'square_credit_card',
				'amazon_payments_advanced',
				'affirm',
				'afterpay',
				'klarna_payments',
				'zipmoney',
			],
			'CA' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'square_credit_card',
				'affirm',
				'afterpay',
				'klarna_payments',
			],
			'AT' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'mollie_wc_gateway_banktransfer',
				'klarna_payments',
				'amazon_payments_advanced',
			],
			'BE' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'mollie_wc_gateway_banktransfer',
				'klarna_payments',
				'amazon_payments_advanced',
			],
			'BG' => [ 'stripe', 'ppcp-gateway' ],
			'HR' => [ 'ppcp-gateway' ],
			'CH' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'mollie_wc_gateway_banktransfer',
				'klarna_payments',
			],
			'CY' => [ 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ],
			'CZ' => [ 'stripe', 'ppcp-gateway' ],
			'DK' => [
				'stripe',
				'ppcp-gateway',
				'klarna_payments',
				'amazon_payments_advanced',
			],
			'EE' => [ 'stripe', 'ppcp-gateway' ],
			'ES' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'mollie_wc_gateway_banktransfer',
				'square_credit_card',
				'klarna_payments',
				'amazon_payments_advanced',
			],
			'FI' => [
				'stripe',
				'ppcp-gateway',
				'mollie_wc_gateway_banktransfer',
				'kco',
				'klarna_payments',
			],
			'FR' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'mollie_wc_gateway_banktransfer',
				'square_credit_card',
				'klarna_payments',
				'amazon_payments_advanced',
			],
			'DE' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'mollie_wc_gateway_banktransfer',
				'klarna_payments',
				'amazon_payments_advanced',
			],
			'GB' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'mollie_wc_gateway_banktransfer',
				'square_credit_card',
				'klarna_payments',
				'amazon_payments_advanced',
			],
			'GR' => [ 'stripe', 'ppcp-gateway' ],
			'HU' => [ 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ],
			'IE' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'square_credit_card',
				'amazon_payments_advanced',
			],
			'IT' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'mollie_wc_gateway_banktransfer',
				'klarna_payments',
				'amazon_payments_advanced',
			],
			'LV' => [ 'stripe', 'ppcp-gateway' ],
			'LT' => [ 'stripe', 'ppcp-gateway' ],
			'LU' => [ 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ],
			'MT' => [ 'stripe', 'ppcp-gateway' ],
			'NL' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'mollie_wc_gateway_banktransfer',
				'klarna_payments',
				'amazon_payments_advanced',
			],
			'NO' => [ 'stripe', 'ppcp-gateway', 'kco', 'klarna_payments' ],
			'PL' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'mollie_wc_gateway_banktransfer',
				'klarna_payments',
			],
			'PT' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'amazon_payments_advanced',
			],
			'RO' => [ 'stripe', 'ppcp-gateway' ],
			'SK' => [ 'stripe', 'ppcp-gateway' ],
			'SL' => [ 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ],
			'SE' => [
				'stripe',
				'ppcp-gateway',
				'kco',
				'klarna_payments',
				'amazon_payments_advanced',
			],
			'MX' => [
				'stripe',
				'woo-mercado-pago-custom',
				'ppcp-gateway',
				'klarna_payments',
			],
			'BR' => [ 'stripe', 'woo-mercado-pago-custom', 'ppcp-gateway' ],
			'AR' => [ 'woo-mercado-pago-custom', 'ppcp-gateway' ],
			'BO' => [],
			'CL' => [ 'woo-mercado-pago-custom', 'ppcp-gateway' ],
			'CO' => [ 'woo-mercado-pago-custom', 'ppcp-gateway' ],
			'EC' => [ 'woo-mercado-pago-custom', 'ppcp-gateway' ],
			'FK' => [],
			'GF' => [],
			'GY' => [],
			'PY' => [],
			'PE' => [ 'woo-mercado-pago-custom', 'ppcp-gateway' ],
			'SR' => [],
			'UY' => [ 'woo-mercado-pago-custom', 'ppcp-gateway' ],
			'VE' => [ 'ppcp-gateway' ],
			'AU' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'square_credit_card',
				'eway',
				'afterpay',
				'klarna_payments',
				'zipmoney',
			],
			'NZ' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'eway',
				'klarna_payments',
				'zipmoney',
			],
			'HK' => [
				'woocommerce_payments',
				'stripe',
				'ppcp-gateway',
				'eway',
				'payoneer-checkout',
			],
			'JP' => [
				'stripe',
				'ppcp-gateway',
				'square_credit_card',
				'amazon_payments_advanced',
			],
			'SG' => [ 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'eway' ],
			'CN' => [ 'ppcp-gateway', 'payoneer-checkout' ],
			'FJ' => [],
			'GU' => [],
			'ID' => [ 'stripe', 'ppcp-gateway' ],
			'IN' => [ 'stripe', 'razorpay', 'payubiz', 'ppcp-gateway' ],
			'ZA' => [ 'payfast', 'paystack' ],
			'NG' => [ 'paystack' ],
			'GH' => [ 'paystack' ],
		);

		// If the country code is not in the list, return default priority.
		if ( ! isset( $recommendation_priority_map[ $country_code ] ) ) {
			return self::get_default_recommendation_priority( $gateway_id );
		}

		$index = array_search( $gateway_id, $recommendation_priority_map[ $country_code ], true );

		// If the gateway is not in the list, return the last index + 1.
		if ( false === $index ) {
			return count( $recommendation_priority_map[ $country_code ] );
		}

		return $index;
	}

	/**
	 * Get the default recommendation priority for a payment gateway.
	 * This is used when a country is not in the $recommendation_priority_map array.
	 *
	 * @param string $id Payment gateway id.
	 * @return int Priority.
	 */
	private static function get_default_recommendation_priority( $id ) {
		if ( ! $id || ! array_key_exists( $id, self::$recommendation_priority ) ) {
			return null;
		}
		return self::$recommendation_priority[ $id ];
	}
}
PaymentGatewaySuggestions/EvaluateSuggestion.php000064400000001534151542725270016302 0ustar00<?php
/**
 * Evaluates the spec and returns a status.
 */

namespace Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RuleEvaluator;

/**
 * Evaluates the spec and returns the evaluated suggestion.
 */
class EvaluateSuggestion {
	/**
	 * Evaluates the spec and returns the suggestion.
	 *
	 * @param object|array $spec The suggestion to evaluate.
	 * @return object The evaluated suggestion.
	 */
	public static function evaluate( $spec ) {
		$rule_evaluator = new RuleEvaluator();
		$suggestion     = is_array( $spec ) ? (object) $spec : clone $spec;

		if ( isset( $suggestion->is_visible ) ) {
			$is_visible             = $rule_evaluator->evaluate( $suggestion->is_visible );
			$suggestion->is_visible = $is_visible;
		}

		return $suggestion;
	}
}
PaymentGatewaySuggestions/Init.php000064400000005517151542725270013374 0ustar00<?php
/**
 * Handles running payment gateway suggestion specs
 */

namespace Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\DefaultPaymentGateways;
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\PaymentGatewaysController;

/**
 * Remote Payment Methods engine.
 * This goes through the specs and gets eligible payment gateways.
 */
class Init {
	/**
	 * Option name for dismissed payment method suggestions.
	 */
	const RECOMMENDED_PAYMENT_PLUGINS_DISMISS_OPTION = 'woocommerce_setting_payments_recommendations_hidden';

	/**
	 * Constructor.
	 */
	public function __construct() {
		PaymentGatewaysController::init();
	}

	/**
	 * Go through the specs and run them.
	 *
	 * @param array|null $specs payment suggestion spec array.
	 * @return array
	 */
	public static function get_suggestions( array $specs = null ) {
		$suggestions = array();
		if ( null === $specs ) {
			$specs = self::get_specs();
		}

		foreach ( $specs as $spec ) {
			$suggestion    = EvaluateSuggestion::evaluate( $spec );
			$suggestions[] = $suggestion;
		}

		return array_values(
			array_filter(
				$suggestions,
				function( $suggestion ) {
					return ! property_exists( $suggestion, 'is_visible' ) || $suggestion->is_visible;
				}
			)
		);

	}

	/**
	 * Delete the specs transient.
	 */
	public static function delete_specs_transient() {
		PaymentGatewaySuggestionsDataSourcePoller::get_instance()->delete_specs_transient();
	}

	/**
	 * Get specs or fetch remotely if they don't exist.
	 */
	public static function get_specs() {
		if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) {
			return apply_filters( 'woocommerce_admin_payment_gateway_suggestion_specs', DefaultPaymentGateways::get_all() );
		}
		$specs = PaymentGatewaySuggestionsDataSourcePoller::get_instance()->get_specs_from_data_sources();

		// Fetch specs if they don't yet exist.
		if ( false === $specs || ! is_array( $specs ) || 0 === count( $specs ) ) {
			return apply_filters( 'woocommerce_admin_payment_gateway_suggestion_specs', DefaultPaymentGateways::get_all() );
		}

		return apply_filters( 'woocommerce_admin_payment_gateway_suggestion_specs', $specs );
	}

	/**
	 * Check if suggestions should be shown in the settings screen.
	 *
	 * @return bool
	 */
	public static function should_display() {
		if ( 'yes' === get_option( self::RECOMMENDED_PAYMENT_PLUGINS_DISMISS_OPTION, 'no' ) ) {
			return false;
		}

		if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) {
			return false;
		}

		return apply_filters( 'woocommerce_allow_payment_recommendations', true );
	}

	/**
	 * Dismiss the suggestions.
	 */
	public static function dismiss() {
		return update_option( self::RECOMMENDED_PAYMENT_PLUGINS_DISMISS_OPTION, 'yes' );
	}
}
PaymentGatewaySuggestions/PaymentGatewaySuggestionsDataSourcePoller.php000064400000002154151542725270023006 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions;

use Automattic\WooCommerce\Admin\DataSourcePoller;

/**
 * Specs data source poller class for payment gateway suggestions.
 */
class PaymentGatewaySuggestionsDataSourcePoller extends DataSourcePoller {

	/**
	 * Data Source Poller ID.
	 */
	const ID = 'payment_gateway_suggestions';

	/**
	 * Default data sources array.
	 */
	const DATA_SOURCES = array(
		'https://woocommerce.com/wp-json/wccom/payment-gateway-suggestions/1.0/suggestions.json',
	);

	/**
	 * Class instance.
	 *
	 * @var Analytics instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			// Add country query param to data sources.
			$base_location = wc_get_base_location();
			$data_sources  = array_map(
				function( $url ) use ( $base_location ) {
					return add_query_arg(
						'country',
						$base_location['country'],
						$url
					);
				},
				self::DATA_SOURCES
			);

			self::$instance = new self( self::ID, $data_sources );
		}
		return self::$instance;
	}
}
PaymentGatewaySuggestions/PaymentGatewaysController.php000064400000010671151542725270017654 0ustar00<?php
/**
 * Logic for extending WC_REST_Payment_Gateways_Controller.
 */

namespace Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions;

use Automattic\WooCommerce\Admin\Features\TransientNotices;

defined( 'ABSPATH' ) || exit;

/**
 * PaymentGateway class
 */
class PaymentGatewaysController {

	/**
	 * Initialize payment gateway changes.
	 */
	public static function init() {
		add_filter( 'woocommerce_rest_prepare_payment_gateway', array( __CLASS__, 'extend_response' ), 10, 3 );
		add_filter( 'admin_init', array( __CLASS__, 'possibly_do_connection_return_action' ) );
		add_action( 'woocommerce_admin_payment_gateway_connection_return', array( __CLASS__, 'handle_successfull_connection' ) );
	}

	/**
	 * Add necessary fields to REST API response.
	 *
	 * @param  WP_REST_Response   $response   Response data.
	 * @param  WC_Payment_Gateway $gateway    Payment gateway object.
	 * @param  WP_REST_Request    $request    Request object.
	 * @return WP_REST_Response
	 */
	public static function extend_response( $response, $gateway, $request ) {
		$data = $response->get_data();

		$data['needs_setup']          = $gateway->needs_setup();
		$data['post_install_scripts'] = self::get_post_install_scripts( $gateway );
		$data['settings_url']         = method_exists( $gateway, 'get_settings_url' )
			? $gateway->get_settings_url()
			: admin_url( 'admin.php?page=wc-settings&tab=checkout&section=' . strtolower( $gateway->id ) );

		$return_url             = wc_admin_url( '&task=payments&connection-return=' . strtolower( $gateway->id ) . '&_wpnonce=' . wp_create_nonce( 'connection-return' ) );
		$data['connection_url'] = method_exists( $gateway, 'get_connection_url' )
			? $gateway->get_connection_url( $return_url )
			: null;

		$data['setup_help_text'] = method_exists( $gateway, 'get_setup_help_text' )
			? $gateway->get_setup_help_text()
			: null;

		$data['required_settings_keys'] = method_exists( $gateway, 'get_required_settings_keys' )
			? $gateway->get_required_settings_keys()
			: array();

		$response->set_data( $data );

		return $response;
	}

	/**
	 * Get payment gateway scripts for post-install.
	 *
	 * @param  WC_Payment_Gateway $gateway Payment gateway object.
	 * @return array Install scripts.
	 */
	public static function get_post_install_scripts( $gateway ) {
		$scripts    = array();
		$wp_scripts = wp_scripts();

		$handles = method_exists( $gateway, 'get_post_install_script_handles' )
			? $gateway->get_post_install_script_handles()
			: array();

		foreach ( $handles as $handle ) {
			if ( isset( $wp_scripts->registered[ $handle ] ) ) {
				$scripts[] = $wp_scripts->registered[ $handle ];
			}
		}

		return $scripts;
	}

	/**
	 * Call an action after a gating has been successfully returned.
	 */
	public static function possibly_do_connection_return_action() {
		if (
			! isset( $_GET['page'] ) ||
			'wc-admin' !== $_GET['page'] ||
			! isset( $_GET['task'] ) ||
			'payments' !== $_GET['task'] ||
			! isset( $_GET['connection-return'] ) ||
			! isset( $_GET['_wpnonce'] ) ||
			! wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wpnonce'] ) ), 'connection-return' )
		) {
			return;
		}

		$gateway_id = sanitize_text_field( wp_unslash( $_GET['connection-return'] ) );

		do_action( 'woocommerce_admin_payment_gateway_connection_return', $gateway_id );
	}

	/**
	 * Handle a successful gateway connection.
	 *
	 * @param string $gateway_id Gateway ID.
	 */
	public static function handle_successfull_connection( $gateway_id ) {
		// phpcs:disable WordPress.Security.NonceVerification
		if ( ! isset( $_GET['success'] ) || 1 !== intval( $_GET['success'] ) ) {
			return;
		}
		// phpcs:enable WordPress.Security.NonceVerification

		$payment_gateways = WC()->payment_gateways()->payment_gateways();
		$payment_gateway  = isset( $payment_gateways[ $gateway_id ] ) ? $payment_gateways[ $gateway_id ] : null;

		if ( ! $payment_gateway ) {
			return;
		}

		$payment_gateway->update_option( 'enabled', 'yes' );

		TransientNotices::add(
			array(
				'user_id' => get_current_user_id(),
				'id'      => 'payment-gateway-connection-return-' . str_replace( ',', '-', $gateway_id ),
				'status'  => 'success',
				'content' => sprintf(
					/* translators: the title of the payment gateway */
					__( '%s connected successfully', 'woocommerce' ),
					$payment_gateway->method_title
				),
			)
		);

		wc_admin_record_tracks_event(
			'tasklist_payment_connect_method',
			array(
				'payment_method' => $gateway_id,
			)
		);

		wp_safe_redirect( wc_admin_url() );
	}
}
ProductBlockEditor/BlockRegistry.php000064400000007700151542725270013620 0ustar00<?php
/**
 * WooCommerce Product Editor Block Registration
 */

namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor;

use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;

/**
 * Product block registration and style registration functionality.
 */
class BlockRegistry {
	/**
	 * The directory where blocks are stored after build.
	 */
	const BLOCKS_DIR = 'product-editor/blocks';

	/**
	 * Array of all available product blocks.
	 */
	const PRODUCT_BLOCKS = [
		'woocommerce/conditional',
		'woocommerce/product-catalog-visibility-field',
		'woocommerce/product-checkbox-field',
		'woocommerce/product-collapsible',
		'woocommerce/product-description-field',
		'woocommerce/product-images-field',
		'woocommerce/product-inventory-email-field',
		'woocommerce/product-sku-field',
		'woocommerce/product-name-field',
		'woocommerce/product-pricing-field',
		'woocommerce/product-radio-field',
		'woocommerce/product-regular-price-field',
		'woocommerce/product-sale-price-field',
		'woocommerce/product-schedule-sale-fields',
		'woocommerce/product-section',
		'woocommerce/product-shipping-class-field',
		'woocommerce/product-shipping-dimensions-fields',
		'woocommerce/product-summary-field',
		'woocommerce/product-tab',
		'woocommerce/product-tag-field',
		'woocommerce/product-inventory-quantity-field',
		'woocommerce/product-toggle-field',
		'woocommerce/product-variation-items-field',
		'woocommerce/product-variations-fields',
		'woocommerce/product-password-field',
		'woocommerce/product-has-variations-notice',
		'woocommerce/product-taxonomy-field',
	];

	/**
	 * Get a file path for a given block file.
	 *
	 * @param string $path File path.
	 */
	private function get_file_path( $path ) {
		return WC_ABSPATH . WCAdminAssets::get_path( 'js' ) . trailingslashit( self::BLOCKS_DIR ) . $path;
	}

	/**
	 * Initialize all blocks.
	 */
	public function init() {
		add_filter( 'block_categories_all', array( $this, 'register_categories' ), 10, 2 );
		$this->register_product_blocks();
	}

	/**
	 * Register all the product blocks.
	 */
	private function register_product_blocks() {
		foreach ( self::PRODUCT_BLOCKS as $block_name ) {
			$this->register_block( $block_name );
		}
	}

	/**
	 * Register product related block categories.
	 *
	 * @param array[]                 $block_categories Array of categories for block types.
	 * @param WP_Block_Editor_Context $editor_context   The current block editor context.
	 */
	public function register_categories( $block_categories, $editor_context ) {
		if ( INIT::EDITOR_CONTEXT_NAME === $editor_context->name ) {
			$block_categories[] = array(
				'slug'  => 'woocommerce',
				'title' => __( 'WooCommerce', 'woocommerce' ),
				'icon'  => null,
			);
		}

		return $block_categories;
	}

	/**
	 * Get the block name without the "woocommerce/" prefix.
	 *
	 * @param string $block_name Block name.
	 *
	 * @return string
	 */
	private function remove_block_prefix( $block_name ) {
		if ( 0 === strpos( $block_name, 'woocommerce/' ) ) {
			return substr_replace( $block_name, '', 0, strlen( 'woocommerce/' ) );
		}

		return $block_name;
	}

	/**
	 * Register a single block.
	 *
	 * @param string $block_name Block name.
	 *
	 * @return WP_Block_Type|false The registered block type on success, or false on failure.
	 */
	private function register_block( $block_name ) {
		$block_name      = $this->remove_block_prefix( $block_name );
		$block_json_file = $this->get_file_path( $block_name . '/block.json' );

		if ( ! file_exists( $block_json_file ) ) {
			return false;
		}

		// phpcs:disable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$metadata = json_decode( file_get_contents( $block_json_file ), true );
		if ( ! is_array( $metadata ) || ! $metadata['name'] ) {
			return false;
		}

		$registry = \WP_Block_Type_Registry::get_instance();

		if ( $registry->is_registered( $metadata['name'] ) ) {
			$registry->unregister( $metadata['name'] );
		}

		return register_block_type_from_metadata( $block_json_file );
	}

}
ProductBlockEditor/Init.php000064400000015121151542725270011734 0ustar00<?php
/**
 * WooCommerce Product Block Editor
 */

namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor;

use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\SimpleProductTemplate;
use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplateRegistry;
use WP_Block_Editor_Context;

/**
 * Loads assets related to the product block editor.
 */
class Init {
	/**
	 * The context name used to identify the editor.
	 */
	const EDITOR_CONTEXT_NAME = 'woocommerce/edit-product';

	/**
	 * Supported post types.
	 *
	 * @var array
	 */
	private $supported_post_types = array( 'simple' );

	/**
	 * Redirection controller.
	 *
	 * @var RedirectionController
	 */
	private $redirection_controller;

	/**
	 * Constructor
	 */
	public function __construct() {
		if ( Features::is_enabled( 'product-variation-management' ) ) {
			array_push( $this->supported_post_types, 'variable' );
		}

		$this->redirection_controller = new RedirectionController( $this->supported_post_types );

		if ( \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ) {
			// Register the product block template.
			$template_registry = wc_get_container()->get( BlockTemplateRegistry::class );
			$template_registry->register( new SimpleProductTemplate() );

			if ( ! Features::is_enabled( 'new-product-management-experience' ) ) {
				add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) );
				add_action( 'admin_enqueue_scripts', array( $this, 'dequeue_conflicting_styles' ), 100 );
				add_action( 'get_edit_post_link', array( $this, 'update_edit_product_link' ), 10, 2 );
			}
			add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) );
			add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
			add_filter( 'woocommerce_register_post_type_product', array( $this, 'add_product_template' ) );

			add_action( 'current_screen', array( $this, 'set_current_screen_to_block_editor_if_wc_admin' ) );

			$block_registry = new BlockRegistry();
			$block_registry->init();

			$tracks = new Tracks();
			$tracks->init();
		}
	}

	/**
	 * Enqueue scripts needed for the product form block editor.
	 */
	public function enqueue_scripts() {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return;
		}
		$post_type_object     = get_post_type_object( 'product' );
		$block_editor_context = new WP_Block_Editor_Context( array( 'name' => self::EDITOR_CONTEXT_NAME ) );

		$editor_settings = array();
		if ( ! empty( $post_type_object->template ) ) {
			$editor_settings['template']                 = $post_type_object->template;
			$editor_settings['templateLock']             = ! empty( $post_type_object->template_lock ) ? $post_type_object->template_lock : false;
		}

		$editor_settings = get_block_editor_settings( $editor_settings, $block_editor_context );

		$script_handle = 'wc-admin-edit-product';
		wp_register_script( $script_handle, '', array(), '0.1.0', true );
		wp_enqueue_script( $script_handle );
		wp_add_inline_script(
			$script_handle,
			'var productBlockEditorSettings = productBlockEditorSettings || ' . wp_json_encode( $editor_settings ) . ';',
			'before'
		);
		wp_add_inline_script(
			$script_handle,
			sprintf( 'wp.blocks.setCategories( %s );', wp_json_encode( $editor_settings['blockCategories'] ) ),
			'before'
		);
		wp_tinymce_inline_scripts();
		wp_enqueue_media();
	}

	/**
	 * Enqueue styles needed for the rich text editor.
	 */
	public function enqueue_styles() {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return;
		}
		wp_enqueue_style( 'wp-edit-blocks' );
		wp_enqueue_style( 'wp-format-library' );
		wp_enqueue_editor();
		/**
		 * Enqueue any block editor related assets.
		 *
		 * @since 7.1.0
		*/
		do_action( 'enqueue_block_editor_assets' );
	}

	/**
	 * Dequeue conflicting styles.
	 */
	public function dequeue_conflicting_styles() {
		if ( ! PageController::is_admin_or_embed_page() ) {
			return;
		}
		// Dequeing this to avoid conflicts, until we remove the 'woocommerce-page' class.
		wp_dequeue_style( 'woocommerce-blocktheme' );
	}

	/**
	 * Update the edit product links when the new experience is enabled.
	 *
	 * @param string $link    The edit link.
	 * @param int    $post_id Post ID.
	 * @return string
	 */
	public function update_edit_product_link( $link, $post_id ) {
		$product = wc_get_product( $post_id );

		if ( ! $product ) {
			return $link;
		}

		if ( $product->get_type() === 'simple' ) {
			return admin_url( 'admin.php?page=wc-admin&path=/product/' . $product->get_id() );
		}

		return $link;
	}

	/**
	 * Enqueue styles needed for the rich text editor.
	 *
	 * @param array $args Array of post type arguments.
	 * @return array Array of post type arguments.
	 */
	public function add_product_template( $args ) {
		if ( ! isset( $args['template'] ) ) {
			// Get the template from the registry.
			$template_registry = wc_get_container()->get( BlockTemplateRegistry::class );
			$template          = $template_registry->get_registered( 'simple-product' );

			if ( isset( $template ) ) {
				$args['template_lock'] = 'all';
				$args['template']      = $template->get_formatted_template();
			}
		}
		return $args;
	}

	/**
	 * Adds fields so that we can store user preferences for the variations block.
	 *
	 * @param array $user_data_fields User data fields.
	 * @return array
	 */
	public function add_user_data_fields( $user_data_fields ) {
		return array_merge(
			$user_data_fields,
			array(
				'variable_product_block_tour_shown',
				'product_block_variable_options_notice_dismissed',
				'variable_items_without_price_notice_dismissed'
			)
		);
	}

	/**
	 * Sets the current screen to the block editor if a wc-admin page.
	 */
	public function set_current_screen_to_block_editor_if_wc_admin() {
		$screen = get_current_screen();

		// phpcs:ignore Squiz.PHP.CommentedOutCode.Found
		// (no idea why I need that phpcs:ignore above, but I'm tired trying to re-write this comment to get it to pass)
		// we can't check the 'path' query param because client-side routing is used within wc-admin,
		// so this action handler is only called on the initial page load from the server, which might
		// not be the product edit page (it mostly likely isn't).
		if ( PageController::is_admin_page() ) {
			$screen->is_block_editor( true );

			wp_add_inline_script(
				'wp-blocks',
				'wp.blocks && wp.blocks.unstable__bootstrapServerSideBlockDefinitions && wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( get_block_editor_server_block_settings() ) . ');'
			);
		}
	}
}
ProductBlockEditor/ProductTemplates/AbstractProductFormTemplate.php000064400000003572151542725270021763 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;

use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
use Automattic\WooCommerce\Internal\Admin\BlockTemplates\AbstractBlockTemplate;

/**
 * Block template class.
 */
abstract class AbstractProductFormTemplate extends AbstractBlockTemplate implements ProductFormTemplateInterface {
	/**
	 * Get the template area.
	 */
	public function get_area(): string {
		return 'product-form';
	}

	/**
	 * Get a group block by ID.
	 *
	 * @param string $group_id The group block ID.
	 * @throws \UnexpectedValueException If block is not of type GroupInterface.
	 */
	public function get_group_by_id( string $group_id ): ?GroupInterface {
		$group = $this->get_block( $group_id );
		if ( $group && ! $group instanceof GroupInterface ) {
			throw new \UnexpectedValueException( 'Block with specified ID is not a group.' );
		}
		return $group;
	}

	/**
	 * Get a section block by ID.
	 *
	 * @param string $section_id The section block ID.
	 * @throws \UnexpectedValueException If block is not of type SectionInterface.
	 */
	public function get_section_by_id( string $section_id ): ?SectionInterface {
		$section = $this->get_block( $section_id );
		if ( $section && ! $section instanceof SectionInterface ) {
			throw new \UnexpectedValueException( 'Block with specified ID is not a section.' );
		}
		return $section;
	}

	/**
	 * Get a block by ID.
	 *
	 * @param string $block_id The block block ID.
	 */
	public function get_block_by_id( string $block_id ): ?BlockInterface {
		return $this->get_block( $block_id );
	}

	/**
	 * Add a custom block type to this template.
	 *
	 * @param array $block_config The block data.
	 */
	public function add_group( array $block_config ): GroupInterface {
		$block = new Group( $block_config, $this->get_root_template(), $this );
		return $this->add_inner_block( $block );
	}
}
ProductBlockEditor/ProductTemplates/Group.php000064400000003727151542725270015435 0ustar00<?php
/**
 * WooCommerce Product Group Block class.
 */

namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;

use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
use Automattic\WooCommerce\Internal\Admin\BlockTemplates\BlockContainerTrait;

/**
 * Class for Group block.
 */
class Group extends ProductBlock implements GroupInterface {
	use BlockContainerTrait;

	/**
	 * Group Block constructor.
	 *
	 * @param array                   $config The block configuration.
	 * @param BlockTemplateInterface  $root_template The block template that this block belongs to.
	 * @param ContainerInterface|null $parent The parent block container.
	 *
	 * @throws \ValueError If the block configuration is invalid.
	 * @throws \ValueError If the parent block container does not belong to the same template as the block.
	 * @throws \InvalidArgumentException If blockName key and value are passed into block configuration.
	 */
	public function __construct( array $config, BlockTemplateInterface &$root_template, ContainerInterface &$parent = null ) {
		if ( ! empty( $config['blockName'] ) ) {
			throw new \InvalidArgumentException( 'Unexpected key "blockName", this defaults to "woocommerce/product-tab".' );
		}
		if ( $config['id'] && ( empty( $config['attributes'] ) || empty( $config['attributes']['id'] ) ) ) {
			$config['attributes']       = empty( $config['attributes'] ) ? [] : $config['attributes'];
			$config['attributes']['id'] = $config['id'];
		}
		parent::__construct( array_merge( array( 'blockName' => 'woocommerce/product-tab' ), $config ), $root_template, $parent );
	}

	/**
	 * Add a section block type to this template.
	 *
	 * @param array $block_config The block data.
	 */
	public function add_section( array $block_config ): SectionInterface {
		$block = new Section( $block_config, $this->get_root_template(), $this );
		return $this->add_inner_block( $block );
	}
}
ProductBlockEditor/ProductTemplates/GroupInterface.php000064400000001312151542725270017242 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;

use Automattic\WooCommerce\Admin\BlockTemplates\BlockContainerInterface;
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;

/**
 * Interface for block containers.
 */
interface GroupInterface extends BlockContainerInterface {

	/**
	 * Adds a new section block.
	 *
	 * @param array $block_config block config.
	 * @return SectionInterface new block section.
	 */
	public function add_section( array $block_config ): SectionInterface;

	/**
	 * Adds a new block to the section block.
	 *
	 * @param array $block_config block config.
	 */
	public function add_block( array $block_config ): BlockInterface;
}
ProductBlockEditor/ProductTemplates/ProductBlock.php000064400000001523151542725270016724 0ustar00<?php
/**
 * WooCommerce Product Block class.
 */

namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;

use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
use Automattic\WooCommerce\Internal\Admin\BlockTemplates\AbstractBlock;
use Automattic\WooCommerce\Internal\Admin\BlockTemplates\BlockContainerTrait;

/**
 * Class for Product block.
 */
class ProductBlock extends AbstractBlock implements ContainerInterface {
	use BlockContainerTrait;
	/**
	 * Adds block to the section block.
	 *
	 * @param array $block_config The block data.
	 */
	public function &add_block( array $block_config ): BlockInterface {
		$block = new ProductBlock( $block_config, $this->get_root_template(), $this );
		return $this->add_inner_block( $block );
	}
}
ProductBlockEditor/ProductTemplates/ProductFormTemplateInterface.php000064400000002124151542725270022110 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;

use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;

/**
 * Interface for block containers.
 */
interface ProductFormTemplateInterface extends BlockTemplateInterface {

	/**
	 * Adds a new group block.
	 *
	 * @param array $block_config block config.
	 * @return BlockInterface new block section.
	 */
	public function add_group( array $block_config ): GroupInterface;

	/**
	 * Gets Group block by id.
	 *
	 * @param string $group_id group id.
	 * @return GroupInterface|null
	 */
	public function get_group_by_id( string $group_id ): ?GroupInterface;

	/**
	 * Gets Section block by id.
	 *
	 * @param string $section_id section id.
	 * @return SectionInterface|null
	 */
	public function get_section_by_id( string $section_id ): ?SectionInterface;

	/**
	 * Gets Block by id.
	 *
	 * @param string $block_id block id.
	 * @return BlockInterface|null
	 */
	public function get_block_by_id( string $block_id ): ?BlockInterface;
}
ProductBlockEditor/ProductTemplates/Section.php000064400000003200151542725270015727 0ustar00<?php
/**
 * WooCommerce Section Block class.
 */

namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;

use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;

/**
 * Class for Section block.
 */
class Section extends ProductBlock implements SectionInterface {

	/**
	 * Section Block constructor.
	 *
	 * @param array                   $config The block configuration.
	 * @param BlockTemplateInterface  $root_template The block template that this block belongs to.
	 * @param ContainerInterface|null $parent The parent block container.
	 *
	 * @throws \ValueError If the block configuration is invalid.
	 * @throws \ValueError If the parent block container does not belong to the same template as the block.
	 * @throws \InvalidArgumentException If blockName key and value are passed into block configuration.
	 */
	public function __construct( array $config, BlockTemplateInterface &$root_template, ContainerInterface &$parent = null ) {
		if ( ! empty( $config['blockName'] ) ) {
			throw new \InvalidArgumentException( 'Unexpected key "blockName", this defaults to "woocommerce/product-section".' );
		}
		parent::__construct( array_merge( array( 'blockName' => 'woocommerce/product-section' ), $config ), $root_template, $parent );
	}

	/**
	 * Add a section block type to this template.
	 *
	 * @param array $block_config The block data.
	 */
	public function add_section( array $block_config ): SectionInterface {
		$block = new Section( $block_config, $this->get_root_template(), $this );
		return $this->add_inner_block( $block );
	}
}
ProductBlockEditor/ProductTemplates/SectionInterface.php000064400000001315151542725270017555 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;

use Automattic\WooCommerce\Admin\BlockTemplates\BlockContainerInterface;
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;


/**
 * Interface for block containers.
 */
interface SectionInterface extends BlockContainerInterface {

	/**
	 * Adds a new section block.
	 *
	 * @param array $block_config block config.
	 * @return SectionInterface new block section.
	 */
	public function add_section( array $block_config ): SectionInterface;

	/**
	 * Adds a new block to the section block.
	 *
	 * @param array $block_config block config.
	 */
	public function add_block( array $block_config ): BlockInterface;
}
ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php000064400000057715151542725270020635 0ustar00<?php
/**
 * SimpleProductTemplate
 */

namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;

use Automattic\WooCommerce\Admin\Features\Features;

/**
 * Simple Product Template.
 */
class SimpleProductTemplate extends AbstractProductFormTemplate implements ProductFormTemplateInterface {
	/**
	 * The context name used to identify the editor.
	 */
	const GROUP_IDS = array(
		'GENERAL'      => 'general',
		'ORGANIZATION' => 'organization',
		'PRICING'      => 'pricing',
		'INVENTORY'    => 'inventory',
		'SHIPPING'     => 'shipping',
		'VARIATIONS'   => 'variations',
	);

	/**
	 * SimpleProductTemplate constructor.
	 */
	public function __construct() {
		$this->add_group_blocks();
		$this->add_general_group_blocks();
		$this->add_organization_group_blocks();
		$this->add_pricing_group_blocks();
		$this->add_inventory_group_blocks();
		$this->add_shipping_group_blocks();
		$this->add_variation_group_blocks();
	}

	/**
	 * Get the template ID.
	 */
	public function get_id(): string {
		return 'simple-product';
	}

	/**
	 * Get the template title.
	 */
	public function get_title(): string {
		return __( 'Simple Product Template', 'woocommerce' );
	}

	/**
	 * Get the template description.
	 */
	public function get_description(): string {
		return __( 'Template for the simple product form', 'woocommerce' );
	}

	/**
	 * Adds the group blocks to the template.
	 */
	private function add_group_blocks() {
		$this->add_group(
			[
				'id'         => $this::GROUP_IDS['GENERAL'],
				'order'      => 10,
				'attributes' => [
					'title' => __( 'General', 'woocommerce' ),
				],
			]
		);
		$this->add_group(
			[
				'id'         => $this::GROUP_IDS['ORGANIZATION'],
				'order'      => 15,
				'attributes' => [
					'title' => __( 'Organization', 'woocommerce' ),
				],
			]
		);
		$this->add_group(
			[
				'id'         => $this::GROUP_IDS['PRICING'],
				'order'      => 20,
				'attributes' => [
					'title' => __( 'Pricing', 'woocommerce' ),
				],
			]
		);
		$this->add_group(
			[
				'id'         => $this::GROUP_IDS['INVENTORY'],
				'order'      => 30,
				'attributes' => [
					'title' => __( 'Inventory', 'woocommerce' ),
				],
			]
		);
		$this->add_group(
			[
				'id'         => $this::GROUP_IDS['SHIPPING'],
				'order'      => 40,
				'attributes' => [
					'title' => __( 'Shipping', 'woocommerce' ),
				],
			]
		);
		if ( Features::is_enabled( 'product-variation-management' ) ) {
			$this->add_group(
				[
					'id'         => $this::GROUP_IDS['VARIATIONS'],
					'order'      => 50,
					'attributes' => [
						'title' => __( 'Variations', 'woocommerce' ),
					],
				]
			);
		}
	}

	/**
	 * Adds the general group blocks to the template.
	 */
	private function add_general_group_blocks() {
		$general_group = $this->get_group_by_id( $this::GROUP_IDS['GENERAL'] );
		// Basic Details Section.
		$basic_details = $general_group->add_section(
			[
				'id'         => 'basic-details',
				'order'      => 10,
				'attributes' => [
					'title'       => __( 'Basic details', 'woocommerce' ),
					'description' => __( 'This info will be displayed on the product page, category pages, social media, and search results.', 'woocommerce' ),
				],
			]
		);
		$basic_details->add_block(
			[
				'id'         => 'product-name',
				'blockName'  => 'woocommerce/product-name-field',
				'order'      => 10,
				'attributes' => [
					'name'      => 'Product name',
					'autoFocus' => true,
				],
			]
		);
		$basic_details->add_block(
			[
				'id'        => 'product-summary',
				'blockName' => 'woocommerce/product-summary-field',
				'order'     => 20,
			]
		);
		$pricing_columns  = $basic_details->add_block(
			[
				'id'        => 'product-pricing-columns',
				'blockName' => 'core/columns',
				'order'     => 30,
			]
		);
		$pricing_column_1 = $pricing_columns->add_block(
			[
				'id'         => 'product-pricing-column-1',
				'blockName'  => 'core/column',
				'order'      => 10,
				'attributes' => [
					'templateLock' => 'all',
				],
			]
		);
		$pricing_column_1->add_block(
			[
				'id'         => 'product-regular-price',
				'blockName'  => 'woocommerce/product-regular-price-field',
				'order'      => 10,
				'attributes' => [
					'name'  => 'regular_price',
					'label' => __( 'List price', 'woocommerce' ),
					/* translators: PricingTab: This is a link tag to the pricing tab. */
					'help'  => __( 'Manage more settings in <PricingTab>Pricing.</PricingTab>', 'woocommerce' ),
				],
			]
		);
		$pricing_column_2 = $pricing_columns->add_block(
			[
				'id'         => 'product-pricing-column-2',
				'blockName'  => 'core/column',
				'order'      => 20,
				'attributes' => [
					'templateLock' => 'all',
				],
			]
		);
		$pricing_column_2->add_block(
			[
				'id'         => 'product-sale-price',
				'blockName'  => 'woocommerce/product-sale-price-field',
				'order'      => 10,
				'attributes' => [
					'label' => __( 'Sale price', 'woocommerce' ),
				],
			]
		);

		// Description section.
		$description_section = $general_group->add_section(
			[
				'id'         => 'product-description-section',
				'order'      => 20,
				'attributes' => [
					'title'       => __( 'Description', 'woocommerce' ),
					'description' => __( 'What makes this product unique? What are its most important features? Enrich the product page by adding rich content using blocks.', 'woocommerce' ),
				],
			]
		);
		$description_section->add_block(
			[
				'id'        => 'product-description',
				'blockName' => 'woocommerce/product-description-field',
				'order'     => 10,
			]
		);
		// Images section.
		$images_section = $general_group->add_section(
			[
				'id'         => 'product-images-section',
				'order'      => 30,
				'attributes' => [
					'title'       => __( 'Images', 'woocommerce' ),
					'description' => sprintf(
					/* translators: %1$s: Images guide link opening tag. %2$s: Images guide link closing tag. */
						__( 'Drag images, upload new ones or select files from your library. For best results, use JPEG files that are 1000 by 1000 pixels or larger. %1$sHow to prepare images?%2$s', 'woocommerce' ),
						'<a href="http://woocommerce.com/#" target="_blank" rel="noreferrer">',
						'</a>'
					),
				],
			]
		);
		$images_section->add_block(
			[
				'id'         => 'product-images',
				'blockName'  => 'woocommerce/product-images-field',
				'order'      => 10,
				'attributes' => [
					'images' => [],
				],
			]
		);
	}

	/**
	 * Adds the organization group blocks to the template.
	 */
	private function add_organization_group_blocks() {
		$organization_group = $this->get_group_by_id( $this::GROUP_IDS['ORGANIZATION'] );
		// Product Catalog Section.
		$product_catalog_section = $organization_group->add_section(
			[
				'id'         => 'product-catalog-section',
				'order'      => 10,
				'attributes' => [
					'title' => __( 'Product catalog', 'woocommerce' ),
				],
			]
		);
		$product_catalog_section->add_block(
			[
				'id'         => 'product-categories',
				'blockName'  => 'woocommerce/product-taxonomy-field',
				'order'      => 10,
				'attributes' => [
					'slug'               => 'product_cat',
					'property'           => 'categories',
					'label'              => __( 'Categories', 'woocommerce' ),
					'createTitle'        => __( 'Create new category', 'woocommerce' ),
					'dialogNameHelpText' => __( 'Shown to customers on the product page.', 'woocommerce' ),
					'parentTaxonomyText' => __( 'Parent category', 'woocommerce' ),
				],
			]
		);
		$product_catalog_section->add_block(
			[
				'id'         => 'product-tags',
				'blockName'  => 'woocommerce/product-tag-field',
				'attributes' => [
					'name' => 'tags',
				],
			]
		);
		$product_catalog_section->add_block(
			[
				'id'         => 'product-catalog-search-visibility',
				'blockName'  => 'woocommerce/product-catalog-visibility-field',
				'order'      => 20,
				'attributes' => [
					'label'      => __( 'Hide in product catalog', 'woocommerce' ),
					'visibility' => 'search',
				],
			]
		);
		$product_catalog_section->add_block(
			[
				'id'         => 'product-catalog-catalog-visibility',
				'blockName'  => 'woocommerce/product-catalog-visibility-field',
				'order'      => 30,
				'attributes' => [
					'label'      => __( 'Hide from search results', 'woocommerce' ),
					'visibility' => 'catalog',
				],
			]
		);
		$product_catalog_section->add_block(
			[
				'id'         => 'product-enable-product-reviews',
				'blockName'  => 'woocommerce/product-checkbox-field',
				'order'      => 40,
				'attributes' => [
					'label'    => __( 'Enable product reviews', 'woocommerce' ),
					'property' => 'reviews_allowed',
				],
			]
		);
		$product_catalog_section->add_block(
			[
				'id'         => 'product-post-password',
				'blockName'  => 'woocommerce/product-password-field',
				'order'      => 50,
				'attributes' => [
					'label' => __( 'Require a password', 'woocommerce' ),
				],
			]
		);
		// Attributes section.
		$product_catalog_section = $organization_group->add_section(
			[
				'id'         => 'product-attributes-section',
				'order'      => 20,
				'attributes' => [
					'title' => __( 'Attributes', 'woocommerce' ),
				],
			]
		);
		$product_catalog_section->add_block(
			[
				'id'        => 'product-attributes',
				'blockName' => 'woocommerce/product-attributes-field',
				'order'     => 10,
			]
		);
	}

	/**
	 * Adds the pricing group blocks to the template.
	 */
	private function add_pricing_group_blocks() {
		$pricing_group = $this->get_group_by_id( $this::GROUP_IDS['PRICING'] );
		$pricing_group->add_block(
			[
				'id'         => 'pricing-has-variations-notice',
				'blockName'  => 'woocommerce/product-has-variations-notice',
				'order'      => 10,
				'attributes' => [
					'content'    => __( 'This product has options, such as size or color. You can now manage each variation\'s price and other details individually.', 'woocommerce' ),
					'buttonText' => __( 'Go to Variations', 'woocommerce' ),
					'type'       => 'info',
				],
			]
		);
		// Product Pricing Section.
		$product_pricing_section = $pricing_group->add_section(
			[
				'id'         => 'product-pricing-section',
				'order'      => 20,
				'attributes' => [
					'title'       => __( 'Pricing', 'woocommerce' ),
					'description' => sprintf(
					/* translators: %1$s: Images guide link opening tag. %2$s: Images guide link closing tag.*/
						__( 'Set a competitive price, put the product on sale, and manage tax calculations. %1$sHow to price your product?%2$s', 'woocommerce' ),
						'<a href="https://woocommerce.com/posts/how-to-price-products-strategies-expert-tips/" target="_blank" rel="noreferrer">',
						'</a>'
					),
					'blockGap'    => 'unit-40',
				],
			]
		);
		$pricing_columns         = $product_pricing_section->add_block(
			[
				'id'        => 'product-pricing-group-pricing-columns',
				'blockName' => 'core/columns',
				'order'     => 10,
			]
		);
		$pricing_column_1        = $pricing_columns->add_block(
			[
				'id'         => 'product-pricing-group-pricing-column-1',
				'blockName'  => 'core/column',
				'order'      => 10,
				'attributes' => [
					'templateLock' => 'all',
				],
			]
		);
		$pricing_column_1->add_block(
			[
				'id'         => 'product-pricing-regular-price',
				'blockName'  => 'woocommerce/product-regular-price-field',
				'order'      => 10,
				'attributes' => [
					'name'  => 'regular_price',
					'label' => __( 'List price', 'woocommerce' ),
				],
			]
		);
		$pricing_column_2 = $pricing_columns->add_block(
			[
				'id'         => 'product-pricing-group-pricing-column-2',
				'blockName'  => 'core/column',
				'order'      => 20,
				'attributes' => [
					'templateLock' => 'all',
				],
			]
		);
		$pricing_column_2->add_block(
			[
				'id'         => 'product-pricing-sale-price',
				'blockName'  => 'woocommerce/product-sale-price-field',
				'order'      => 10,
				'attributes' => [
					'label' => __( 'Sale price', 'woocommerce' ),
				],
			]
		);
		$product_pricing_section->add_block(
			[
				'id'        => 'product-pricing-schedule-sale-fields',
				'blockName' => 'woocommerce/product-schedule-sale-fields',
				'order'     => 20,
			]
		);
		$product_pricing_section->add_block(
			[
				'id'         => 'product-sale-tax',
				'blockName'  => 'woocommerce/product-radio-field',
				'order'      => 30,
				'attributes' => [
					'title'    => __( 'Charge sales tax on', 'woocommerce' ),
					'property' => 'tax_status',
					'options'  => [
						[
							'label' => __( 'Product and shipping', 'woocommerce' ),
							'value' => 'taxable',
						],
						[
							'label' => __( 'Only shipping', 'woocommerce' ),
							'value' => 'shipping',
						],
						[
							'label' => __( "Don't charge tax", 'woocommerce' ),
							'value' => 'none',
						],
					],
				],
			]
		);
		$pricing_advanced_block = $product_pricing_section->add_block(
			[
				'id'         => 'product-pricing-advanced',
				'blockName'  => 'woocommerce/product-collapsible',
				'order'      => 40,
				'attributes' => [
					'toggleText'       => __( 'Advanced', 'woocommerce' ),
					'initialCollapsed' => true,
					'persistRender'    => true,
				],
			]
		);
		$pricing_advanced_block->add_block(
			[
				'id'         => 'product-tax-class',
				'blockName'  => 'woocommerce/product-radio-field',
				'order'      => 10,
				'attributes' => [
					'title'       => __( 'Tax class', 'woocommerce' ),
					'description' => sprintf(
					/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
						__( 'Apply a tax rate if this product qualifies for tax reduction or exemption. %1$sLearn more%2$s.', 'woocommerce' ),
						'<a href="https://woocommerce.com/document/setting-up-taxes-in-woocommerce/#shipping-tax-class" target="_blank" rel="noreferrer">',
						'</a>'
					),
					'property'    => 'tax_class',
					'options'     => [
						[
							'label' => __( 'Standard', 'woocommerce' ),
							'value' => '',
						],
						[
							'label' => __( 'Reduced rate', 'woocommerce' ),
							'value' => 'reduced-rate',
						],
						[
							'label' => __( 'Zero rate', 'woocommerce' ),
							'value' => 'zero-rate',
						],
					],
				],
			]
		);
	}

	/**
	 * Adds the inventory group blocks to the template.
	 */
	private function add_inventory_group_blocks() {
		$inventory_group = $this->get_group_by_id( $this::GROUP_IDS['INVENTORY'] );
		$inventory_group->add_block(
			[
				'id'         => 'product_variation_notice_inventory_tab',
				'blockName'  => 'woocommerce/product-has-variations-notice',
				'order'      => 10,
				'attributes' => [
					'content'    => __( 'This product has options, such as size or color. You can now manage each variation\'s price and other details individually.', 'woocommerce' ),
					'buttonText' => __( 'Go to Variations', 'woocommerce' ),
					'type'       => 'info',
				],
			]
		);
		// Product Pricing Section.
		$product_inventory_section       = $inventory_group->add_section(
			[
				'id'         => 'product-inventory-section',
				'order'      => 20,
				'attributes' => [
					'title'       => __( 'Inventory', 'woocommerce' ),
					'description' => sprintf(
					/* translators: %1$s: Inventory settings link opening tag. %2$s: Inventory settings link closing tag.*/
						__( 'Set up and manage inventory for this product, including status and available quantity. %1$sManage store inventory settings%2$s', 'woocommerce' ),
						'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=products&section=inventory' ) . '" target="_blank" rel="noreferrer">',
						'</a>'
					),
					'blockGap'    => 'unit-40',
				],
			]
		);
		$product_inventory_inner_section = $product_inventory_section->add_section(
			[
				'id'    => 'product-inventory-inner-section',
				'order' => 10,
			]
		);
		$product_inventory_inner_section->add_block(
			[
				'id'        => 'product-sku-field',
				'blockName' => 'woocommerce/product-sku-field',
				'order'     => 10,
			]
		);
		$product_inventory_inner_section->add_block(
			[
				'id'         => 'product-track-stock',
				'blockName'  => 'woocommerce/product-toggle-field',
				'order'      => 20,
				'attributes' => [
					'label'    => __( 'Track stock quantity for this product', 'woocommerce' ),
					'property' => 'manage_stock',
					'disabled' => 'yes' !== get_option( 'woocommerce_manage_stock' ),
					'disabledCopy' => sprintf(
						/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
							__( 'Per your %1$sstore settings%2$s, inventory management is <strong>disabled</strong>.', 'woocommerce' ),
							'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=products&section=inventory' ) . '" target="_blank" rel="noreferrer">',
							'</a>'
						),
				],
			]
		);
		$product_inventory_quantity_conditional = $product_inventory_inner_section->add_block(
			[
				'id'         => 'product-inventory-quantity-conditional-wrapper',
				'blockName'  => 'woocommerce/conditional',
				'order'      => 30,
				'attributes' => [
					'mustMatch' => [
						'manage_stock' => [ true ],
					],
				],
			]
		);
		$product_inventory_quantity_conditional->add_block(
			[
				'id'        => 'product-inventory-quantity',
				'blockName' => 'woocommerce/product-inventory-quantity-field',
				'order'     => 10,
			]
		);
		$product_stock_status_conditional = $product_inventory_section->add_block(
			[
				'id'         => 'product-stock-status-conditional-wrapper',
				'blockName'  => 'woocommerce/conditional',
				'order'      => 20,
				'attributes' => [
					'mustMatch' => [
						'manage_stock' => [ false ],
					],
				],
			]
		);
		$product_stock_status_conditional->add_block(
			[
				'id'         => 'product-stock-status',
				'blockName'  => 'woocommerce/product-radio-field',
				'order'      => 10,
				'attributes' => [
					'title'    => __( 'Stock status', 'woocommerce' ),
					'property' => 'stock_status',
					'options'  => [
						[
							'label' => __( 'In stock', 'woocommerce' ),
							'value' => 'instock',
						],
						[
							'label' => __( 'Out of stock', 'woocommerce' ),
							'value' => 'outofstock',
						],
						[
							'label' => __( 'On backorder', 'woocommerce' ),
							'value' => 'onbackorder',
						],
					],
				],
			]
		);
		$product_inventory_advanced         = $product_inventory_section->add_block(
			[
				'id'         => 'product-inventory-advanced',
				'blockName'  => 'woocommerce/product-collapsible',
				'order'      => 30,
				'attributes' => [
					'toggleText'       => __( 'Advanced', 'woocommerce' ),
					'initialCollapsed' => true,
					'persistRender'    => true,
				],
			]
		);
		$product_inventory_advanced_wrapper = $product_inventory_advanced->add_block(
			[
				'blockName'  => 'woocommerce/product-section',
				'order'      => 10,
				'attributes' => [
					'blockGap' => 'unit-40',
				],
			]
		);
		$product_out_of_stock_conditional   = $product_inventory_advanced_wrapper->add_block(
			[
				'id'         => 'product-out-of-stock-conditional-wrapper',
				'blockName'  => 'woocommerce/conditional',
				'order'      => 10,
				'attributes' => [
					'mustMatch' => [
						'manage_stock' => [ true ],
					],
				],
			]
		);
		$product_out_of_stock_conditional->add_block(
			[
				'id'         => 'product-out-of-stock',
				'blockName'  => 'woocommerce/product-radio-field',
				'order'      => 10,
				'attributes' => [
					'title'    => __( 'When out of stock', 'woocommerce' ),
					'property' => 'backorders',
					'options'  => [
						[
							'label' => __( 'Allow purchases', 'woocommerce' ),
							'value' => 'yes',
						],
						[
							'label' => __(
								'Allow purchases, but notify customers',
								'woocommerce'
							),
							'value' => 'notify',
						],
						[
							'label' => __( "Don't allow purchases", 'woocommerce' ),
							'value' => 'no',
						],
					],
				],
			]
		);
		$product_out_of_stock_conditional->add_block(
			[
				'id'        => 'product-inventory-email',
				'blockName' => 'woocommerce/product-inventory-email-field',
				'order'     => 20,
			]
		);

		$product_inventory_advanced_wrapper->add_block(
			[
				'id'         => 'product-limit-purchase',
				'blockName'  => 'woocommerce/product-checkbox-field',
				'order'      => 20,
				'attributes' => [
					'title'    => __(
						'Restrictions',
						'woocommerce'
					),
					'label'    => __(
						'Limit purchases to 1 item per order',
						'woocommerce'
					),
					'property' => 'sold_individually',
					'tooltip'  => __(
						'When checked, customers will be able to purchase only 1 item in a single order. This is particularly useful for items that have limited quantity, like art or handmade goods.',
						'woocommerce'
					),
				],
			]
		);
	}

	/**
	 * Adds the shipping group blocks to the template.
	 */
	private function add_shipping_group_blocks() {
		$shipping_group = $this->get_group_by_id( $this::GROUP_IDS['SHIPPING'] );
		$shipping_group->add_block(
			[
				'id'         => 'product_variation_notice_shipping_tab',
				'blockName'  => 'woocommerce/product-has-variations-notice',
				'order'      => 10,
				'attributes' => [
					'content'    => __( 'This product has options, such as size or color. You can now manage each variation\'s price and other details individually.', 'woocommerce' ),
					'buttonText' => __( 'Go to Variations', 'woocommerce' ),
					'type'       => 'info',
				],
			]
		);
		// Product Pricing Section.
		$product_fee_and_dimensions_section = $shipping_group->add_section(
			[
				'id'         => 'product-fee-and-dimensions-section',
				'order'      => 20,
				'attributes' => [
					'title'       => __( 'Fees & dimensions', 'woocommerce' ),
					'description' => sprintf(
					/* translators: %1$s: How to get started? link opening tag. %2$s: How to get started? link closing tag.*/
						__( 'Set up shipping costs and enter dimensions used for accurate rate calculations. %1$sHow to get started?%2$s.', 'woocommerce' ),
						'<a href="https://woocommerce.com/posts/how-to-calculate-shipping-costs-for-your-woocommerce-store/" target="_blank" rel="noreferrer">',
						'</a>'
					),
				],
			]
		);
		$product_fee_and_dimensions_section->add_block(
			[
				'id'        => 'product-shipping-class',
				'blockName' => 'woocommerce/product-shipping-class-field',
				'order'     => 10,
			]
		);
		$product_fee_and_dimensions_section->add_block(
			[
				'id'        => 'product-shipping-dimensions',
				'blockName' => 'woocommerce/product-shipping-dimensions-fields',
				'order'     => 20,
			]
		);
	}

	/**
	 * Adds the variation group blocks to the template.
	 */
	private function add_variation_group_blocks() {
		$variation_group = $this->get_group_by_id( $this::GROUP_IDS['VARIATIONS'] );
		if ( ! $variation_group ) {
			return;
		}
		$variation_fields = $variation_group->add_block(
			[
				'id'         => 'product_variation-field-group',
				'blockName'  => 'woocommerce/product-variations-fields',
				'order'      => 10,
				'attributes' => [
					'description' => sprintf(
					/* translators: %1$s: Sell your product in multiple variations like size or color. strong opening tag. %2$s: Sell your product in multiple variations like size or color. strong closing tag.*/
						__( '%1$sSell your product in multiple variations like size or color.%2$s Get started by adding options for the buyers to choose on the product page.', 'woocommerce' ),
						'<strong>',
						'</strong>'
					),
				],
			]
		);
		$variation_options_section = $variation_fields->add_block(
			[
				'id'         => 'product-variation-options-section',
				'blockName'  => 'woocommerce/product-section',
				'order'      => 10,
				'attributes' => [
					'title' => __( 'Variation options', 'woocommerce' ),
				],
			]
		);
		$variation_options_section->add_block(
			[
				'id'        => 'product-variation-options',
				'blockName' => 'woocommerce/product-variations-options-field',
			]
		);
		$variation_section = $variation_fields->add_block(
			[
				'id'         => 'product-variation-section',
				'blockName'  => 'woocommerce/product-section',
				'order'      => 20,
				'attributes' => [
					'title' => __( 'Variations', 'woocommerce' ),
				],
			]
		);

		$variation_section->add_block(
			[
				'id'        => 'product-variation-items',
				'blockName' => 'woocommerce/product-variation-items-field',
				'order'     => 10,
			]
		);
	}
}
ProductBlockEditor/RedirectionController.php000064400000010131151542725270015340 0ustar00<?php
/**
 * WooCommerce Product Editor Redirection Controller
 */

namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor;

use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;

/**
 * Handle redirecting to the old or new editor based on features and support.
 */
class RedirectionController {

	/**
	 * Supported post types.
	 *
	 * @var array
	 */
	private $supported_post_types;

	/**
	 * Set up the hooks used for redirection.
	 *
	 * @param array $supported_post_types Array of supported post types.
	 */
	public function __construct( $supported_post_types ) {
		$this->supported_post_types = $supported_post_types;

		if ( \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ) {
			add_action( 'current_screen', array( $this, 'maybe_redirect_to_new_editor' ), 30, 0 );
			add_action( 'current_screen', array( $this, 'redirect_non_supported_product_types' ), 30, 0 );
		} else {
			add_action( 'current_screen', array( $this, 'maybe_redirect_to_old_editor' ), 30, 0 );
		}
	}

	/**
	 * Check if the current screen is the legacy add product screen.
	 */
	protected function is_legacy_add_new_screen(): bool {
		$screen = get_current_screen();
		return 'post' === $screen->base && 'product' === $screen->post_type && 'add' === $screen->action;
	}

	/**
	 * Check if the current screen is the legacy edit product screen.
	 */
	protected function is_legacy_edit_screen(): bool {
		$screen = get_current_screen();
		return 'post' === $screen->base
			&& 'product' === $screen->post_type
			&& isset( $_GET['post'] )
			&& isset( $_GET['action'] )
			&& 'edit' === $_GET['action'];
	}

	/**
	 * Check if a product is supported by the new experience.
	 *
	 * @param integer $product_id Product ID.
	 */
	protected function is_product_supported( $product_id ): bool {
		$product = $product_id ? wc_get_product( $product_id ) : null;
		$digital_product = $product->is_downloadable() || $product->is_virtual();
		return $product && in_array( $product->get_type(), $this->supported_post_types, true ) && ! $digital_product;
	}

	/**
	 * Redirects from old product form to the new product form if the
	 * feature `product_block_editor` is enabled.
	 */
	public function maybe_redirect_to_new_editor(): void {
		if ( $this->is_legacy_add_new_screen() ) {
			wp_safe_redirect( admin_url( 'admin.php?page=wc-admin&path=/add-product' ) );
			exit();
		}

		if ( $this->is_legacy_edit_screen() ) {
			$product_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : null;
			if ( ! $this->is_product_supported( $product_id ) ) {
				return;
			}
			wp_safe_redirect( admin_url( 'admin.php?page=wc-admin&path=/product/' . $product_id ) );
			exit();
		}
	}

	/**
	 * Redirects from new product form to the old product form if the
	 * feature `product_block_editor` is enabled.
	 */
	public function maybe_redirect_to_old_editor(): void {
		$route = $this->get_parsed_route();

		if ( 'add-product' === $route['page'] ) {
			wp_safe_redirect( admin_url( 'post-new.php?post_type=product' ) );
			exit();
		}

		if ( 'product' === $route['page'] ) {
			wp_safe_redirect( admin_url( 'post.php?post=' . $route['product_id'] . '&action=edit' ) );
			exit();
		}
	}

	/**
	 * Get the parsed WooCommerce Admin path.
	 */
	protected function get_parsed_route(): array {
		if ( ! \Automattic\WooCommerce\Admin\PageController::is_admin_page() || ! isset( $_GET['path'] ) ) {
			return array(
				'page'       => null,
				'product_id' => null,
			);
		}

		$path        = esc_url_raw( wp_unslash( $_GET['path'] ) );
		$path_pieces = explode( '/', wp_parse_url( $path, PHP_URL_PATH ) );

		return array(
			'page'       => $path_pieces[1],
			'product_id' => 'product' === $path_pieces[1] ? absint( $path_pieces[2] ) : null,
		);
	}

	/**
	 * Redirect non supported product types to legacy editor.
	 */
	public function redirect_non_supported_product_types(): void {
		$route      = $this->get_parsed_route();
		$product_id = $route['product_id'];

		if ( 'product' === $route['page'] && ! $this->is_product_supported( $product_id ) ) {
			wp_safe_redirect( admin_url( 'post.php?post=' . $route['product_id'] . '&action=edit' ) );
			exit();
		}
	}

}
ProductBlockEditor/Tracks.php000064400000002263151542725270012263 0ustar00<?php
/**
 * WooCommerce Product Block Editor
 */

namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor;

/**
 * Add tracks for the product block editor.
 */
class Tracks {

	/**
	 * Initialize the tracks.
	 */
	public function init() {
		add_filter( 'woocommerce_product_source', array( $this, 'add_product_source' ) );
	}

	/**
	 * Check if a URL is a product editor page.
	 *
	 * @param string $url Url to check.
	 * @return boolean
	 */
	protected function is_product_editor_page( $url ) {
		$query_string = wp_parse_url( wp_get_referer(), PHP_URL_QUERY );
		parse_str( $query_string, $query );

		if ( ! isset( $query['page'] ) || 'wc-admin' !== $query['page'] || ! isset( $query['path'] ) ) {
			return false;
		}

		$path_pieces = explode( '/', $query['path'] );
		$route       = $path_pieces[1];

		return 'add-product' === $route || 'product' === $route;
	}

	/**
	 * Update the product source if we're on the product editor page.
	 *
	 * @param string $source Source of product.
	 * @return string
	 */
	public function add_product_source( $source ) {
		if ( $this->is_product_editor_page( wp_get_referer() ) ) {
			return 'product-block-editor-v1';
		}

		return $source;
	}

}
ShippingPartnerSuggestions/DefaultShippingPartners.php000064400000020727151542725270017454 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\ShippingPartnerSuggestions;

/**
 * Default Shipping Partners
 */
class DefaultShippingPartners {

	/**
	 * Get default specs.
	 *
	 * @return array Default specs.
	 */
	public static function get_all() {
		$asset_base_url         = WC()->plugin_url() . '/assets/images/shipping_partners/';
		$column_layout_features = array(
			array(
				'icon'        => $asset_base_url . 'timer.svg',
				'title'       => __( 'Save time', 'woocommerce' ),
				'description' => __(
					'Automatically import order information to quickly print your labels.',
					'woocommerce'
				),
			),
			array(
				'icon'        => $asset_base_url . 'discount.svg',
				'title'       => __( 'Save money', 'woocommerce' ),
				'description' => __(
					'Shop for the best shipping rates, and access pre-negotiated discounted rates.',
					'woocommerce'
				),
			),
			array(
				'icon'        => $asset_base_url . 'star.svg',
				'title'       => __( 'Wow your shoppers', 'woocommerce' ),
				'description' => __(
					'Keep your customers informed with tracking notifications.',
					'woocommerce'
				),
			),
		);

		$check_icon = $asset_base_url . 'check.svg';

		return array(
			array(
				'name'              => 'ShipStation',
				'slug'              => 'woocommerce-shipstation-integration',
				'description'       => __( 'Powerful yet easy-to-use solution:', 'woocommerce' ),
				'layout_column'     => array(
					'image'    => $asset_base_url . 'shipstation-column.svg',
					'features' => $column_layout_features,
				),
				'layout_row'        => array(
					'image'    => $asset_base_url . 'shipstation-row.svg',
					'features' => array(
						array(
							'icon'        => $check_icon,
							'description' => __(
								'Print labels from Royal Mail, Parcel Force, DPD, and many more',
								'woocommerce'
							),
						),
						array(
							'icon'        => $check_icon,
							'description' => __(
								'Shop for the best rates, in real-time',
								'woocommerce'
							),
						),
						array(
							'icon'        => $check_icon,
							'description' => __( 'Connect selling channels easily', 'woocommerce' ),
						),
						array(
							'icon'        => $check_icon,
							'description' => __( 'Advance automated workflows', 'woocommerce' ),
						),
						array(
							'icon'        => $check_icon,
							'description' => __( '30-days free trial', 'woocommerce' ),
						),
					),
				),
				'learn_more_link'   => 'https://wordpress.org/plugins/woocommerce-shipstation-integration/',
				'is_visible'        => array(
					self::get_rules_for_countries( array( 'AU', 'CA', 'GB' ) ),
				),
				'available_layouts' => array( 'row', 'column' ),
			),
			array(
				'name'              => 'Skydropx',
				'slug'              => 'skydropx-cotizador-y-envios',
				'layout_column'     => array(
					'image'    => $asset_base_url . 'skydropx-column.svg',
					'features' => $column_layout_features,
				),
				'description'       => '',
				'learn_more_link'   => 'https://wordpress.org/plugins/skydropx-cotizador-y-envios/',
				'is_visible'        => array(
					self::get_rules_for_countries( array( 'MX', 'CO' ) ),
				),
				'available_layouts' => array( 'column' ),
			),
			array(
				'name'              => 'Envia',
				'slug'              => '',
				'description'       => '',
				'layout_column'     => array(
					'image'    => $asset_base_url . 'envia-column.svg',
					'features' => $column_layout_features,
				),
				'learn_more_link'   => 'https://woocommerce.com/products/envia-shipping-and-fulfillment/',
				'is_visible'        => array(
					self::get_rules_for_countries( array( 'CL', 'AR', 'PE', 'BR', 'UY', 'GT' ) ),
				),
				'available_layouts' => array( 'column' ),
			),
			array(
				'name'              => 'Sendcloud',
				'slug'              => 'sendcloud-shipping',
				'description'       => __( 'All-in-one shipping tool:', 'woocommerce' ),
				'layout_column'     => array(
					'image'    => $asset_base_url . 'sendcloud-column.svg',
					'features' => $column_layout_features,
				),
				'layout_row'        => array(
					'image'    => $asset_base_url . 'sendcloud-row.svg',
					'features' => array(
						array(
							'icon'        => $check_icon,
							'description' => __( 'Print labels from 80+ carriers', 'woocommerce' ),
						),
						array(
							'icon'        => $check_icon,
							'description' => __(
								'Process orders in just a few clicks',
								'woocommerce'
							),
						),
						array(
							'icon'        => $check_icon,
							'description' => __( 'Customize checkout options', 'woocommerce' ),
						),

						array(
							'icon'        => $check_icon,
							'description' => __( 'Self-service tracking & returns', 'woocommerce' ),
						),
						array(
							'icon'        => $check_icon,
							'description' => __( 'Start with a free plan', 'woocommerce' ),
						),
					),
				),
				'learn_more_link'   => 'https://wordpress.org/plugins/sendcloud-shipping/',
				'is_visible'        => array(
					self::get_rules_for_countries( array( 'NL', 'AT', 'BE', 'FR', 'DE', 'ES', 'GB', 'IT' ) ),
				),
				'available_layouts' => array( 'row', 'column' ),
			),
			array(
				'name'              => 'Packlink',
				'slug'              => 'packlink-pro-shipping',
				'description'       => __( 'Optimize your full shipping process:', 'woocommerce' ),
				'layout_column'     => array(
					'image'    => $asset_base_url . 'packlink-column.svg',
					'features' => $column_layout_features,
				),
				'layout_row'        => array(
					'image'    => $asset_base_url . 'packlink-row.svg',
					'features' => array(
						array(
							'icon'        => $check_icon,
							'description' => __(
								'Automated, real-time order import',
								'woocommerce'
							),
						),
						array(
							'icon'        => $check_icon,
							'description' => __(
								'Direct access to leading carriers',
								'woocommerce'
							),
						),
						array(
							'icon'        => $check_icon,
							'description' => __(
								'Access competitive shipping prices',
								'woocommerce'
							),
						),
						array(
							'icon'        => $check_icon,
							'description' => __( 'Quickly bulk print labels', 'woocommerce' ),
						),
						array(
							'icon'        => $check_icon,
							'description' => __( 'Free shipping platform', 'woocommerce' ),
						),
					),
				),
				'learn_more_link'   => 'https://wordpress.org/plugins/packlink-pro-shipping/',
				'is_visible'        => array(
					self::get_rules_for_countries( array( 'FR', 'DE', 'ES', 'IT' ) ),
				),
				'available_layouts' => array( 'row', 'column' ),
			),
			array(
				'name'              => 'WooCommerce Shipping',
				'slug'              => 'woocommerce-services',
				'description'       => __( 'Save time and money by printing your shipping labels right from your computer with WooCommerce Shipping. Try WooCommerce Shipping for free.', 'woocommerce' ),
				'dependencies'      => array( 'jetpack' ),
				'layout_column'     => array(
					'image'    => $asset_base_url . 'wcs-column.svg',
					'features' => array(
						array(
							'icon'        => $asset_base_url . 'printer.svg',
							'title'       => __( 'Buy postage when you need it', 'woocommerce' ),
							'description' => __( 'No need to wonder where that stampbook went.', 'woocommerce' ),
						),
						array(
							'icon'        => $asset_base_url . 'paper.svg',
							'title'       => __( 'Print at home', 'woocommerce' ),
							'description' => __( 'Pick up an order, then just pay, print, package and post.', 'woocommerce' ),
						),
						array(
							'icon'        => $asset_base_url . 'discount.svg',
							'title'       => __( 'Discounted rates', 'woocommerce' ),
							'description' => __( 'Access discounted shipping rates with DHL and USPS.', 'woocommerce' ),
						),
					),
				),
				'learn_more_link'   => 'https://woocommerce.com/products/shipping/',
				'is_visible'        => array(
					self::get_rules_for_countries( array( 'US' ) ),
				),
				'available_layouts' => array( 'column' ),
			),
		);
	}

	/**
	 * Get rules that match the store base location to one of the provided countries.
	 *
	 * @param array $countries Array of countries to match.
	 * @return object Rules to match.
	 */
	public static function get_rules_for_countries( $countries ) {
		$rules = array();

		foreach ( $countries as $country ) {
			$rules[] = (object) array(
				'type'      => 'base_location_country',
				'value'     => $country,
				'operation' => '=',
			);
		}

		return (object) array(
			'type'     => 'or',
			'operands' => $rules,
		);
	}
}
ShippingPartnerSuggestions/ShippingPartnerSuggestions.php000064400000003676151542725270020223 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\ShippingPartnerSuggestions;

use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RuleEvaluator;

/**
 * Class ShippingPartnerSuggestions
 */
class ShippingPartnerSuggestions {

	/**
	 * Go through the specs and run them.
	 *
	 * @param array|null $specs shipping partner suggestion spec array.
	 * @return array
	 */
	public static function get_suggestions( $specs = null ) {
		$suggestions = array();
		if ( null === $specs ) {
			$specs = self::get_specs_from_datasource();
		}

		$rule_evaluator = new RuleEvaluator();
		foreach ( $specs as &$spec ) {
			$spec = is_array( $spec ) ? (object) $spec : $spec;
			if ( isset( $spec->is_visible ) ) {
				$is_visible = $rule_evaluator->evaluate( $spec->is_visible );
				if ( $is_visible ) {
					$spec->is_visible = true;
					$suggestions[]    = $spec;
				}
			}
		}

		return $suggestions;
	}

	/**
	 * Get specs or fetch remotely if they don't exist.
	 */
	public static function get_specs_from_datasource() {
		if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) {
			/**
			 * It can be used to modify shipping partner suggestions spec.
			 *
			 * @since 7.4.1
			 */
			return apply_filters( 'woocommerce_admin_shipping_partner_suggestions_specs', DefaultShippingPartners::get_all() );
		}
		$specs = ShippingPartnerSuggestionsDataSourcePoller::get_instance()->get_specs_from_data_sources();

		// Fetch specs if they don't yet exist.
		if ( false === $specs || ! is_array( $specs ) || 0 === count( $specs ) ) {
			/**
			 * It can be used to modify shipping partner suggestions spec.
			 *
			 * @since 7.4.1
			 */
			return apply_filters( 'woocommerce_admin_shipping_partner_suggestions_specs', DefaultShippingPartners::get_all() );
		}

		/**
		 * It can be used to modify shipping partner suggestions spec.
		 *
		 * @since 7.4.1
		 */
		return apply_filters( 'woocommerce_admin_shipping_partner_suggestions_specs', $specs );
	}
}
ShippingPartnerSuggestions/ShippingPartnerSuggestionsDataSourcePoller.php000064400000001552151542725270023343 0ustar00<?php

namespace Automattic\WooCommerce\Admin\Features\ShippingPartnerSuggestions;

use Automattic\WooCommerce\Admin\DataSourcePoller;

/**
 * Specs data source poller class for shipping partner suggestions.
 */
class ShippingPartnerSuggestionsDataSourcePoller extends DataSourcePoller {

	/**
	 * Data Source Poller ID.
	 */
	const ID = 'shipping_partner_suggestions';

	/**
	 * Default data sources array.
	 */
	const DATA_SOURCES = array(
		'https://woocommerce.com/wp-json/wccom/shipping-partner-suggestions/1.0/suggestions.json',
	);

	/**
	 * Class instance.
	 *
	 * @var ShippingPartnerSuggestionsDataSourcePoller instance
	 */
	protected static $instance = null;

	/**
	 * Get class instance.
	 */
	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self( self::ID, self::DATA_SOURCES );
		}
		return self::$instance;
	}
}
TransientNotices.php000064400000005450151542725270010567 0ustar00<?php
/**
 * WooCommerce Transient Notices
 */

namespace Automattic\WooCommerce\Admin\Features;

use Automattic\WooCommerce\Internal\Admin\Loader;

/**
 * Shows print shipping label banner on edit order page.
 */
class TransientNotices {

	/**
	 * Option name for the queue.
	 */
	const QUEUE_OPTION = 'woocommerce_admin_transient_notices_queue';

	/**
	 * Constructor
	 */
	public function __construct() {
		add_filter( 'woocommerce_admin_preload_options', array( $this, 'preload_options' ) );
	}


	/**
	 * Get all notices in the queue.
	 *
	 * @return array
	 */
	public static function get_queue() {
		return get_option( self::QUEUE_OPTION, array() );
	}

	/**
	 * Get all notices in the queue by a given user ID.
	 *
	 * @param int $user_id User ID.
	 * @return array
	 */
	public static function get_queue_by_user( $user_id ) {
		$notices = self::get_queue();

		return array_filter(
			$notices,
			function( $notice ) use ( $user_id ) {
				return ! isset( $notice['user_id'] ) ||
					null === $notice['user_id'] ||
					$user_id === $notice['user_id'];
			}
		);
	}

	/**
	 * Get a notice by ID.
	 *
	 * @param array $notice_id Notice of ID to get.
	 * @return array|null
	 */
	public static function get( $notice_id ) {
		$queue = self::get_queue();

		if ( isset( $queue[ $notice_id ] ) ) {
			return $queue[ $notice_id ];
		}

		return null;
	}

	/**
	 * Add a notice to be shown.
	 *
	 * @param array $notice Notice.
	 *    $notice = array(
	 *      'id'      => (string) Unique ID for the notice. Required.
	 *      'user_id' => (int|null) User ID to show the notice to.
	 *      'status'  => (string) info|error|success
	 *      'content' => (string) Content to be shown for the notice. Required.
	 *      'options' => (array) Array of options to be passed to the notice component.
	 *       See https://developer.wordpress.org/block-editor/reference-guides/data/data-core-notices/#createNotice for available options.
	 *    ).
	 */
	public static function add( $notice ) {
		$queue = self::get_queue();

		$defaults               = array(
			'user_id' => null,
			'status'  => 'info',
			'options' => array(),
		);
		$notice_data            = array_merge( $defaults, $notice );
		$notice_data['options'] = (object) $notice_data['options'];

		$queue[ $notice['id'] ] = $notice_data;
		update_option( self::QUEUE_OPTION, $queue );
	}

	/**
	 * Remove a notice by ID.
	 *
	 * @param array $notice_id Notice of ID to remove.
	 */
	public static function remove( $notice_id ) {
		$queue = self::get_queue();
		unset( $queue[ $notice_id ] );
		update_option( self::QUEUE_OPTION, $queue );
	}

	/**
	 * Preload options to prime state of the application.
	 *
	 * @param array $options Array of options to preload.
	 * @return array
	 */
	public function preload_options( $options ) {
		$options[] = self::QUEUE_OPTION;

		return $options;
	}

}
FeaturesController.php000064400000133367151544301670011122 0ustar00<?php
/**
 * FeaturesController class file
 */

namespace Automattic\WooCommerce\Internal\Features;

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\Admin\Analytics;
use Automattic\WooCommerce\Admin\Features\Navigation\Init;
use Automattic\WooCommerce\Admin\Features\NewProductManagementExperience;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\PluginUtil;

defined( 'ABSPATH' ) || exit;

/**
 * Class to define the WooCommerce features that can be enabled and disabled by admin users,
 * provides also a mechanism for WooCommerce plugins to declare that they are compatible
 * (or incompatible) with a given feature.
 */
class FeaturesController {

	use AccessiblePrivateMethods;

	public const FEATURE_ENABLED_CHANGED_ACTION = 'woocommerce_feature_enabled_changed';

	/**
	 * The existing feature definitions.
	 *
	 * @var array[]
	 */
	private $features;

	/**
	 * The registered compatibility info for WooCommerce plugins, with plugin names as keys.
	 *
	 * @var array
	 */
	private $compatibility_info_by_plugin;

	/**
	 * Ids of the legacy features (they existed before the features engine was implemented).
	 *
	 * @var array
	 */
	private $legacy_feature_ids;

	/**
	 * The registered compatibility info for WooCommerce plugins, with feature ids as keys.
	 *
	 * @var array
	 */
	private $compatibility_info_by_feature;

	/**
	 * The LegacyProxy instance to use.
	 *
	 * @var LegacyProxy
	 */
	private $proxy;

	/**
	 * The PluginUtil instance to use.
	 *
	 * @var PluginUtil
	 */
	private $plugin_util;

	/**
	 * Flag indicating that features will be enableable from the settings page
	 * even when they are incompatible with active plugins.
	 *
	 * @var bool
	 */
	private $force_allow_enabling_features = false;

	/**
	 * Flag indicating that plugins will be activable from the plugins page
	 * even when they are incompatible with enabled features.
	 *
	 * @var bool
	 */
	private $force_allow_enabling_plugins = false;

	/**
	 * Creates a new instance of the class.
	 */
	public function __construct() {
		$hpos_enable_sync   = DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION;
		$hpos_authoritative = CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION;
		$features           = array(
			'analytics'            => array(
				'name'               => __( 'Analytics', 'woocommerce' ),
				'description'        => __( 'Enable WooCommerce Analytics', 'woocommerce' ),
				'is_experimental'    => false,
				'enabled_by_default' => true,
				'disable_ui'         => false,
			),
			'new_navigation'       => array(
				'name'            => __( 'Navigation', 'woocommerce' ),
				'description'     => __( 'Add the new WooCommerce navigation experience to the dashboard', 'woocommerce' ),
				'is_experimental' => false,
				'disable_ui'      => false,
			),
			'product_block_editor' => array(
				'name'            => __( 'New product editor', 'woocommerce' ),
				'description'     => __( 'Try the new product editor (Beta)', 'woocommerce' ),
				'is_experimental' => true,
				'disable_ui'      => false,
			),
			// Options for HPOS features are added in CustomOrdersTableController to keep the logic in same place.
			'custom_order_tables'    => array( // This exists for back-compat only, otherwise it's value is superseded by $hpos_authoritative option.
				'name'               => __( 'High-Performance Order Storage (HPOS)', 'woocommerce' ),
				'enabled_by_default' => false,
			),
			$hpos_authoritative    => array(
				'name'             => __( 'High-Performance Order Storage', 'woocommerce' ),
				'order'            => 10,
			),
			$hpos_enable_sync      => array(
				'name'            => '',
				'order'            => 9,
			),
			'cart_checkout_blocks' => array(
				'name'            => __( 'Cart & Checkout Blocks', 'woocommerce' ),
				'description'     => __( 'Optimize for faster checkout', 'woocommerce' ),
				'is_experimental' => false,
				'disable_ui'      => true,
			),
			'marketplace'          => array(
				'name'               => __( 'Marketplace', 'woocommerce' ),
				'description'        => __(
					'New, faster way to find extensions and themes for your WooCommerce store',
					'woocommerce'
				),
				'is_experimental'    => false,
				'enabled_by_default' => true,
				'disable_ui'         => false,
			),
		);

		$this->legacy_feature_ids = array(
			'analytics',
			'new_navigation',
			'product_block_editor',
			'marketplace',
			// Compatibility for COT is determined by `custom_order_tables'.
			CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
			DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
		);

		$this->init_features( $features );

		self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
		self::add_filter( 'added_option', array( $this, 'process_added_option' ), 999, 3 );
		self::add_filter( 'woocommerce_get_sections_advanced', array( $this, 'add_features_section' ), 10, 1 );
		self::add_filter( 'woocommerce_get_settings_advanced', array( $this, 'add_feature_settings' ), 10, 2 );
		self::add_filter( 'deactivated_plugin', array( $this, 'handle_plugin_deactivation' ), 10, 1 );
		self::add_filter( 'all_plugins', array( $this, 'filter_plugins_list' ), 10, 1 );
		self::add_action( 'admin_notices', array( $this, 'display_notices_in_plugins_page' ), 10, 0 );
		self::add_action( 'load-plugins.php', array( $this, 'maybe_invalidate_cached_plugin_data' ) );
		self::add_action( 'after_plugin_row', array( $this, 'handle_plugin_list_rows' ), 10, 2 );
		self::add_action( 'current_screen', array( $this, 'enqueue_script_to_fix_plugin_list_html' ), 10, 1 );
		self::add_filter( 'views_plugins', array( $this, 'handle_plugins_page_views_list' ), 10, 1 );
		self::add_filter( 'woocommerce_admin_shared_settings', array( $this, 'set_change_feature_enable_nonce' ), 20, 1 );
		self::add_action( 'admin_init', array( $this, 'change_feature_enable_from_query_params' ), 20, 0 );
	}

	/**
	 * Initialize the class according to the existing features.
	 *
	 * @param array $features Information about the existing features.
	 */
	private function init_features( array $features ) {
		$this->compatibility_info_by_plugin  = array();
		$this->compatibility_info_by_feature = array();

		$this->features = $features;

		foreach ( array_keys( $this->features ) as $feature_id ) {
			$this->compatibility_info_by_feature[ $feature_id ] = array(
				'compatible'   => array(),
				'incompatible' => array(),
			);
		}
	}

	/**
	 * Initialize the class instance.
	 *
	 * @internal
	 *
	 * @param LegacyProxy $proxy The instance of LegacyProxy to use.
	 * @param PluginUtil  $plugin_util The instance of PluginUtil to use.
	 */
	final public function init( LegacyProxy $proxy, PluginUtil $plugin_util ) {
		$this->proxy       = $proxy;
		$this->plugin_util = $plugin_util;
	}

	/**
	 * Get all the existing WooCommerce features.
	 *
	 * Returns an associative array where keys are unique feature ids
	 * and values are arrays with these keys:
	 *
	 * - name (string)
	 * - description (string)
	 * - is_experimental (bool)
	 * - is_enabled (bool) (only if $include_enabled_info is passed as true)
	 *
	 * @param bool $include_experimental Include also experimental/work in progress features in the list.
	 * @param bool $include_enabled_info True to include the 'is_enabled' field in the returned features info.
	 * @returns array An array of information about existing features.
	 */
	public function get_features( bool $include_experimental = false, bool $include_enabled_info = false ): array {
		$features = $this->features;

		if ( ! $include_experimental ) {
			$features = array_filter(
				$features,
				function( $feature ) {
					return ! $feature['is_experimental'];
				}
			);
		}

		if ( $include_enabled_info ) {
			foreach ( array_keys( $features ) as $feature_id ) {
				$is_enabled                            = $this->feature_is_enabled( $feature_id );
				$features[ $feature_id ]['is_enabled'] = $is_enabled;
			}
		}

		return $features;
	}

	/**
	 * Check if a given feature is currently enabled.
	 *
	 * @param  string $feature_id Unique feature id.
	 * @return bool True if the feature is enabled, false if not or if the feature doesn't exist.
	 */
	public function feature_is_enabled( string $feature_id ): bool {
		if ( ! $this->feature_exists( $feature_id ) ) {
			return false;
		}

		$default_value = $this->feature_is_enabled_by_default( $feature_id ) ? 'yes' : 'no';
		$value         = 'yes' === get_option( $this->feature_enable_option_name( $feature_id ), $default_value );
		return $value;
	}

	/**
	 * Check if a given feature is enabled by default.
	 *
	 * @param string $feature_id Unique feature id.
	 * @return boolean TRUE if the feature is enabled by default, FALSE otherwise.
	 */
	private function feature_is_enabled_by_default( string $feature_id ): bool {
		return ! empty( $this->features[ $feature_id ]['enabled_by_default'] );
	}

	/**
	 * Change the enabled/disabled status of a feature.
	 *
	 * @param string $feature_id Unique feature id.
	 * @param bool   $enable True to enable the feature, false to disable it.
	 * @return bool True on success, false if feature doesn't exist or the new value is the same as the old value.
	 */
	public function change_feature_enable( string $feature_id, bool $enable ): bool {
		if ( ! $this->feature_exists( $feature_id ) ) {
			return false;
		}

		return update_option( $this->feature_enable_option_name( $feature_id ), $enable ? 'yes' : 'no' );
	}

	/**
	 * Declare (in)compatibility with a given feature for a given plugin.
	 *
	 * This method MUST be executed from inside a handler for the 'before_woocommerce_init' hook.
	 *
	 * The plugin name is expected to be in the form 'directory/file.php' and be one of the keys
	 * of the array returned by 'get_plugins', but this won't be checked. Plugins are expected to use
	 * FeaturesUtil::declare_compatibility instead, passing the full plugin file path instead of the plugin name.
	 *
	 * @param string $feature_id Unique feature id.
	 * @param string $plugin_name Plugin name, in the form 'directory/file.php'.
	 * @param bool   $positive_compatibility True if the plugin declares being compatible with the feature, false if it declares being incompatible.
	 * @return bool True on success, false on error (feature doesn't exist or not inside the required hook).
	 * @throws \Exception A plugin attempted to declare itself as compatible and incompatible with a given feature at the same time.
	 */
	public function declare_compatibility( string $feature_id, string $plugin_name, bool $positive_compatibility = true ): bool {
		if ( ! $this->proxy->call_function( 'doing_action', 'before_woocommerce_init' ) ) {
			$class_and_method = ( new \ReflectionClass( $this ) )->getShortName() . '::' . __FUNCTION__;
			/* translators: 1: class::method 2: before_woocommerce_init */
			$this->proxy->call_function( 'wc_doing_it_wrong', $class_and_method, sprintf( __( '%1$s should be called inside the %2$s action.', 'woocommerce' ), $class_and_method, 'before_woocommerce_init' ), '7.0' );
			return false;
		}

		if ( ! $this->feature_exists( $feature_id ) ) {
			return false;
		}

		$plugin_name = str_replace( '\\', '/', $plugin_name );

		// Register compatibility by plugin.

		ArrayUtil::ensure_key_is_array( $this->compatibility_info_by_plugin, $plugin_name );

		$key          = $positive_compatibility ? 'compatible' : 'incompatible';
		$opposite_key = $positive_compatibility ? 'incompatible' : 'compatible';
		ArrayUtil::ensure_key_is_array( $this->compatibility_info_by_plugin[ $plugin_name ], $key );
		ArrayUtil::ensure_key_is_array( $this->compatibility_info_by_plugin[ $plugin_name ], $opposite_key );

		if ( in_array( $feature_id, $this->compatibility_info_by_plugin[ $plugin_name ][ $opposite_key ], true ) ) {
			throw new \Exception( "Plugin $plugin_name is trying to declare itself as $key with the '$feature_id' feature, but it already declared itself as $opposite_key" );
		}

		if ( ! in_array( $feature_id, $this->compatibility_info_by_plugin[ $plugin_name ][ $key ], true ) ) {
			$this->compatibility_info_by_plugin[ $plugin_name ][ $key ][] = $feature_id;
		}

		// Register compatibility by feature.

		$key = $positive_compatibility ? 'compatible' : 'incompatible';

		if ( ! in_array( $plugin_name, $this->compatibility_info_by_feature[ $feature_id ][ $key ], true ) ) {
			$this->compatibility_info_by_feature[ $feature_id ][ $key ][] = $plugin_name;
		}

		return true;
	}

	/**
	 * Check whether a feature exists with a given id.
	 *
	 * @param string $feature_id The feature id to check.
	 * @return bool True if the feature exists.
	 */
	private function feature_exists( string $feature_id ): bool {
		return isset( $this->features[ $feature_id ] );
	}

	/**
	 * Get the ids of the features that a certain plugin has declared compatibility for.
	 *
	 * This method can't be called before the 'woocommerce_init' hook is fired.
	 *
	 * @param string $plugin_name Plugin name, in the form 'directory/file.php'.
	 * @param bool   $enabled_features_only True to return only names of enabled plugins.
	 * @return array An array having a 'compatible' and an 'incompatible' key, each holding an array of feature ids.
	 */
	public function get_compatible_features_for_plugin( string $plugin_name, bool $enabled_features_only = false ) : array {
		$this->verify_did_woocommerce_init( __FUNCTION__ );

		$features = $this->features;
		if ( $enabled_features_only ) {
			$features = array_filter(
				$features,
				array( $this, 'feature_is_enabled' ),
				ARRAY_FILTER_USE_KEY
			);
		}

		if ( ! isset( $this->compatibility_info_by_plugin[ $plugin_name ] ) ) {
			return array(
				'compatible'   => array(),
				'incompatible' => array(),
				'uncertain'    => array_keys( $features ),
			);
		}

		$info                 = $this->compatibility_info_by_plugin[ $plugin_name ];
		$info['compatible']   = array_values( array_intersect( array_keys( $features ), $info['compatible'] ) );
		$info['incompatible'] = array_values( array_intersect( array_keys( $features ), $info['incompatible'] ) );
		$info['uncertain']    = array_values( array_diff( array_keys( $features ), $info['compatible'], $info['incompatible'] ) );

		return $info;
	}

	/**
	 * Get the names of the plugins that have been declared compatible or incompatible with a given feature.
	 *
	 * @param string $feature_id Feature id.
	 * @param bool   $active_only True to return only active plugins.
	 * @return array An array having a 'compatible' and an 'incompatible' key, each holding an array of plugin names.
	 */
	public function get_compatible_plugins_for_feature( string $feature_id, bool $active_only = false ) : array {
		$this->verify_did_woocommerce_init( __FUNCTION__ );

		$woo_aware_plugins = $this->plugin_util->get_woocommerce_aware_plugins( $active_only );
		if ( ! $this->feature_exists( $feature_id ) ) {
			return array(
				'compatible'   => array(),
				'incompatible' => array(),
				'uncertain'    => $woo_aware_plugins,
			);
		}

		$info              = $this->compatibility_info_by_feature[ $feature_id ];
		$info['uncertain'] = array_values( array_diff( $woo_aware_plugins, $info['compatible'], $info['incompatible'] ) );

		return $info;
	}

	/**
	 * Check if the 'woocommerce_init' has run or is running, do a 'wc_doing_it_wrong' if not.
	 *
	 * @param string|null $function Name of the invoking method, if not null, 'wc_doing_it_wrong' will be invoked if 'woocommerce_init' has not run and is not running.
	 * @return bool True if 'woocommerce_init' has run or is running, false otherwise.
	 */
	private function verify_did_woocommerce_init( string $function = null ): bool {
		if ( ! $this->proxy->call_function( 'did_action', 'woocommerce_init' ) &&
			! $this->proxy->call_function( 'doing_action', 'woocommerce_init' ) ) {
			if ( ! is_null( $function ) ) {
				$class_and_method = ( new \ReflectionClass( $this ) )->getShortName() . '::' . $function;
				/* translators: 1: class::method 2: plugins_loaded */
				$this->proxy->call_function( 'wc_doing_it_wrong', $class_and_method, sprintf( __( '%1$s should not be called before the %2$s action.', 'woocommerce' ), $class_and_method, 'woocommerce_init' ), '7.0' );
			}
			return false;
		}

		return true;
	}

	/**
	 * Get the name of the option that enables/disables a given feature.
	 * Note that it doesn't check if the feature actually exists.
	 *
	 * @param string $feature_id The id of the feature.
	 * @return string The option that enables or disables the feature.
	 */
	public function feature_enable_option_name( string $feature_id ): string {
		switch ( $feature_id ) {
			case 'analytics':
				return Analytics::TOGGLE_OPTION_NAME;
			case 'new_navigation':
				return Init::TOGGLE_OPTION_NAME;
			case 'custom_order_tables':
			case CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION:
				return CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION;
			case DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION:
				return DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION;
			default:
				return "woocommerce_feature_{$feature_id}_enabled";
		}
	}

	/**
	 * Checks whether a feature id corresponds to a legacy feature
	 * (a feature that existed prior to the implementation of the features engine).
	 *
	 * @param string $feature_id The feature id to check.
	 * @return bool True if the id corresponds to a legacy feature.
	 */
	public function is_legacy_feature( string $feature_id ): bool {
		return in_array( $feature_id, $this->legacy_feature_ids, true );
	}

	/**
	 * Sets a flag indicating that it's allowed to enable features for which incompatible plugins are active
	 * from the WooCommerce feature settings page.
	 */
	public function allow_enabling_features_with_incompatible_plugins(): void {
		$this->force_allow_enabling_features = true;
	}

	/**
	 * Sets a flag indicating that it's allowed to activate plugins for which incompatible features are enabled
	 * from the WordPress plugins page.
	 */
	public function allow_activating_plugins_with_incompatible_features(): void {
		$this->force_allow_enabling_plugins = true;
	}

	/**
	 * Handler for the 'added_option' hook.
	 *
	 * It fires FEATURE_ENABLED_CHANGED_ACTION when a feature is enabled or disabled.
	 *
	 * @param string $option The option that has been created.
	 * @param mixed  $value The value of the option.
	 */
	private function process_added_option( string $option, $value ) {
		$this->process_updated_option( $option, false, $value );
	}

	/**
	 * Handler for the 'updated_option' hook.
	 *
	 * It fires FEATURE_ENABLED_CHANGED_ACTION when a feature is enabled or disabled.
	 *
	 * @param string $option The option that has been modified.
	 * @param mixed  $old_value The old value of the option.
	 * @param mixed  $value The new value of the option.
	 */
	private function process_updated_option( string $option, $old_value, $value ) {
		$matches = array();
		$success = preg_match( '/^woocommerce_feature_([a-zA-Z0-9_]+)_enabled$/', $option, $matches );

		$known_features = array(
			Analytics::TOGGLE_OPTION_NAME,
			Init::TOGGLE_OPTION_NAME,
			NewProductManagementExperience::TOGGLE_OPTION_NAME,
			DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
			CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
		);

		if ( ! $success && ! in_array( $option, $known_features, true ) ) {
			return;
		}

		if ( $value === $old_value ) {
			return;
		}

		if ( Analytics::TOGGLE_OPTION_NAME === $option ) {
			$feature_id = 'analytics';
		} elseif ( Init::TOGGLE_OPTION_NAME === $option ) {
			$feature_id = 'new_navigation';
		} elseif ( in_array( $option, $known_features, true ) ) {
			$feature_id = $option;
		} else {
			$feature_id = $matches[1];
		}

		/**
		 * Action triggered when a feature is enabled or disabled (the value of the corresponding setting option is changed).
		 *
		 * @param string $feature_id The id of the feature.
		 * @param bool $enabled True if the feature has been enabled, false if it has been disabled.
		 *
		 * @since 7.0.0
		 */
		do_action( self::FEATURE_ENABLED_CHANGED_ACTION, $feature_id, 'yes' === $value );
	}

	/**
	 * Handler for the 'woocommerce_get_sections_advanced' hook,
	 * it adds the "Features" section to the advanced settings page.
	 *
	 * @param array $sections The original sections array.
	 * @return array The updated sections array.
	 */
	private function add_features_section( $sections ) {
		if ( ! isset( $sections['features'] ) ) {
			$sections['features'] = __( 'Features', 'woocommerce' );
		}
		return $sections;
	}

	/**
	 * Handler for the 'woocommerce_get_settings_advanced' hook,
	 * it adds the settings UI for all the existing features.
	 *
	 * Note that the settings added via the 'woocommerce_settings_features' hook will be
	 * displayed in the non-experimental features section.
	 *
	 * @param array  $settings The existing settings for the corresponding settings section.
	 * @param string $current_section The section to get the settings for.
	 * @return array The updated settings array.
	 */
	private function add_feature_settings( $settings, $current_section ): array {
		if ( 'features' !== $current_section ) {
			return $settings;
		}

		// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
		/**
		 * Filter allowing WooCommerce Admin to be disabled.
		 *
		 * @param bool $disabled False.
		 */
		$admin_features_disabled = apply_filters( 'woocommerce_admin_disabled', false );
		// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment

		$feature_settings =
			array(
				array(
					'title' => __( 'Features', 'woocommerce' ),
					'type'  => 'title',
					'desc'  => __( 'Start using new features that are being progressively rolled out to improve the store management experience.', 'woocommerce' ),
					'id'    => 'features_options',
				),
			);

		$features = $this->get_features( true );

		$feature_ids              = array_keys( $features );
		usort( $feature_ids, function( $feature_id_a, $feature_id_b ) use ( $features ) {
			return ( $features[ $feature_id_b ]['order'] ?? 0 ) <=> ( $features[ $feature_id_a ]['order'] ?? 0 );
		} );
		$experimental_feature_ids = array_filter(
			$feature_ids,
			function( $feature_id ) use ( $features ) {
				return $features[ $feature_id ]['is_experimental'] ?? false;
			}
		);
		$mature_feature_ids       = array_diff( $feature_ids, $experimental_feature_ids );
		$feature_ids              = array_merge( $mature_feature_ids, array( 'mature_features_end' ), $experimental_feature_ids );

		foreach ( $feature_ids as $id ) {
			if ( 'mature_features_end' === $id ) {
				// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
				/**
				 * Filter allowing to add additional settings to the WooCommerce Advanced - Features settings page.
				 *
				 * @param bool $disabled False.
				 */
				$feature_settings = apply_filters( 'woocommerce_settings_features', $feature_settings );
				// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment

				if ( ! empty( $experimental_feature_ids ) ) {
					$feature_settings[] = array(
						'type' => 'sectionend',
						'id'   => 'features_options',
					);

					$feature_settings[] = array(
						'title' => __( 'Experimental features', 'woocommerce' ),
						'type'  => 'title',
						'desc'  => __( 'These features are either experimental or incomplete, enable them at your own risk!', 'woocommerce' ),
						'id'    => 'experimental_features_options',
					);
				}
				continue;
			}

			if ( isset( $features[ $id ]['disable_ui'] ) && $features[ $id ]['disable_ui'] ) {
				continue;
			}

			$feature_settings[] = $this->get_setting_for_feature( $id, $features[ $id ], $admin_features_disabled );
		}

		$feature_settings[] = array(
			'type' => 'sectionend',
			'id'   => empty( $experimental_feature_ids ) ? 'features_options' : 'experimental_features_options',
		);

		return $feature_settings;
	}

	/**
	 * Get the parameters to display the setting enable/disable UI for a given feature.
	 *
	 * @param string $feature_id The feature id.
	 * @param array  $feature The feature parameters, as returned by get_features.
	 * @param bool   $admin_features_disabled True if admin features have been disabled via 'woocommerce_admin_disabled' filter.
	 * @return array The parameters to add to the settings array.
	 */
	private function get_setting_for_feature( string $feature_id, array $feature, bool $admin_features_disabled ): array {
		$description = $feature['description'] ?? '';
		$disabled    = false;
		$desc_tip    = '';
		$tooltip     = $feature['tooltip'] ?? '';
		$type        = $feature['type'] ?? 'checkbox';

		if ( ( 'analytics' === $feature_id || 'new_navigation' === $feature_id ) && $admin_features_disabled ) {
			$disabled = true;
			$desc_tip = __( 'WooCommerce Admin has been disabled', 'woocommerce' );
		} elseif ( 'new_navigation' === $feature_id ) {
			$disabled = ! $this->feature_is_enabled( $feature_id );

			if ( $disabled ) {
				$update_text = sprintf(
				// translators: 1: line break tag.
					__( '%1$s The development of this feature is currently on hold.', 'woocommerce' ),
					'<br/>'
				);
			} else {
				$update_text = sprintf(
				// translators: 1: line break tag.
					__(
						'%1$s This navigation will soon become unavailable while we make necessary improvements.
			             If you turn it off now, you will not be able to turn it back on.',
						'woocommerce'
					),
					'<br/>'
				);
			}

			$needs_update = version_compare( get_bloginfo( 'version' ), '5.6', '<' );
			if ( $needs_update && current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) {
				$update_text = sprintf(
				// translators: 1: line break tag, 2: open link to WordPress update link, 3: close link tag.
					__( '%1$s %2$sUpdate WordPress to enable the new navigation%3$s', 'woocommerce' ),
					'<br/>',
					'<a href="' . self_admin_url( 'update-core.php' ) . '" target="_blank">',
					'</a>'
				);
				$disabled = true;
			}

			if ( ! empty( $update_text ) ) {
				$description .= $update_text;
			}
		}

		if ( 'product_block_editor' === $feature_id ) {
			$disabled = version_compare( get_bloginfo( 'version' ), '6.2', '<' );
			if ( $disabled ) {
				$desc_tip = __( '⚠ This feature is compatible with WordPress version 6.2 or higher.', 'woocommerce' );
			}
		}

		if ( ! $this->is_legacy_feature( $feature_id ) && ! $disabled && $this->verify_did_woocommerce_init() ) {
			$disabled                = ! $this->feature_is_enabled( $feature_id );
			$plugin_info_for_feature = $this->get_compatible_plugins_for_feature( $feature_id, true );
			$desc_tip                = $this->plugin_util->generate_incompatible_plugin_feature_warning( $feature_id, $plugin_info_for_feature );
		}

		/**
		 * Filter to customize the description tip that appears under the description of each feature in the features settings page.
		 *
		 * @since 7.1.0
		 *
		 * @param string $desc_tip The original description tip.
		 * @param string $feature_id The id of the feature for which the description tip is being customized.
		 * @param bool $disabled True if the UI currently prevents changing the enable/disable status of the feature.
		 * @return string The new description tip to use.
		 */
		$desc_tip = apply_filters( 'woocommerce_feature_description_tip', $desc_tip, $feature_id, $disabled );

		$feature_setting = array(
			'title'    => $feature['name'],
			'desc'     => $description,
			'type'     => $type,
			'id'       => $this->feature_enable_option_name( $feature_id ),
			'disabled' => $disabled && ! $this->force_allow_enabling_features,
			'desc_tip' => $desc_tip,
			'tooltip'  => $tooltip,
			'default'  => $this->feature_is_enabled_by_default( $feature_id ) ? 'yes' : 'no',
		);

		/**
		 * Allows to modify feature setting that will be used to render in the feature page.
		 *
		 * @param array $feature_setting The feature setting. Describes the feature:
		 *      - title: The title of the feature.
		 *      - desc: The description of the feature. Will be displayed under the title.
		 *      - type: The type of the feature. Could be any of supported settings types from `WC_Admin_Settings::output_fields`, but if it's anything other than checkbox or radio, it will need custom handling.
		 *      - id: The id of the feature. Will be used as the name of the setting.
		 *      - disabled: Whether the feature is disabled or not.
		 *      - desc_tip: The description tip of the feature. Will be displayed as a tooltip next to the description.
		 *      - tooltip: The tooltip of the feature. Will be displayed as a tooltip next to the name.
		 *      - default: The default value of the feature.
		 * @param string $feature_id The id of the feature.
		 * @since 8.0.0
		 */
		return apply_filters( 'woocommerce_feature_setting', $feature_setting, $feature_id );
	}

	/**
	 * Handle the plugin deactivation hook.
	 *
	 * @param string $plugin_name Name of the plugin that has been deactivated.
	 */
	private function handle_plugin_deactivation( $plugin_name ): void {
		unset( $this->compatibility_info_by_plugin[ $plugin_name ] );

		foreach ( array_keys( $this->compatibility_info_by_feature ) as $feature ) {
			$compatibles = $this->compatibility_info_by_feature[ $feature ]['compatible'];
			$this->compatibility_info_by_feature[ $feature ]['compatible'] = array_diff( $compatibles, array( $plugin_name ) );

			$incompatibles = $this->compatibility_info_by_feature[ $feature ]['incompatible'];
			$this->compatibility_info_by_feature[ $feature ]['incompatible'] = array_diff( $incompatibles, array( $plugin_name ) );

		}
	}

	/**
	 * Handler for the all_plugins filter.
	 *
	 * Returns the list of plugins incompatible with a given plugin
	 * if we are in the plugins page and the query string of the current request
	 * looks like '?plugin_status=incompatible_with_feature&feature_id=<feature id>'.
	 *
	 * @param array $list The original list of plugins.
	 */
	private function filter_plugins_list( $list ): array {
		if ( ! $this->verify_did_woocommerce_init() ) {
			return $list;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
		if ( ! function_exists( 'get_current_screen' ) || get_current_screen() && 'plugins' !== get_current_screen()->id || 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) {
			return $list;
		}

		$feature_id = $_GET['feature_id'] ?? 'all';
		if ( 'all' !== $feature_id && ! $this->feature_exists( $feature_id ) ) {
			return $list;
		}

		return $this->get_incompatible_plugins( $feature_id, $list );
	}

	/**
	 * Returns the list of plugins incompatible with a given feature.
	 *
	 * @param string $feature_id ID of the feature. Can also be `all` to denote all features.
	 * @param array  $list       List of plugins to filter.
	 *
	 * @return array List of plugins incompatible with the given feature.
	 */
	private function get_incompatible_plugins( $feature_id, $list ) {
		$incompatibles = array();

		// phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
		foreach ( array_keys( $list ) as $plugin_name ) {
			if ( ! $this->plugin_util->is_woocommerce_aware_plugin( $plugin_name ) || ! $this->proxy->call_function( 'is_plugin_active', $plugin_name ) ) {
				continue;
			}

			$compatibility     = $this->get_compatible_features_for_plugin( $plugin_name );
			$incompatible_with = array_diff(
				array_merge( $compatibility['incompatible'], $compatibility['uncertain'] ),
				$this->legacy_feature_ids
			);

			if ( ( 'all' === $feature_id && ! empty( $incompatible_with ) ) || in_array( $feature_id, $incompatible_with, true ) ) {
				$incompatibles[] = $plugin_name;
			}
		}

		return array_intersect_key( $list, array_flip( $incompatibles ) );
	}

	/**
	 * Handler for the admin_notices action.
	 */
	private function display_notices_in_plugins_page(): void {
		if ( ! $this->verify_did_woocommerce_init() ) {
			return;
		}

		$feature_filter_description_shown = $this->maybe_display_current_feature_filter_description();
		if ( ! $feature_filter_description_shown ) {
			$this->maybe_display_feature_incompatibility_warning();
		}
	}

	/**
	 * Shows a warning when there are any incompatibility between active plugins and enabled features.
	 * The warning is shown in on any admin screen except the plugins screen itself, since
	 * there's already a "You are viewing
	 */
	private function maybe_display_feature_incompatibility_warning(): void {
		if ( ! current_user_can( 'activate_plugins' ) ) {
			return;
		}

		$incompatible_plugins = false;

		foreach ( $this->plugin_util->get_woocommerce_aware_plugins( true ) as $plugin ) {
			$compatibility     = $this->get_compatible_features_for_plugin( $plugin, true );
			$incompatible_with = array_diff(
				array_merge( $compatibility['incompatible'], $compatibility['uncertain'] ),
				$this->legacy_feature_ids
			);

			if ( $incompatible_with ) {
				$incompatible_plugins = true;
				break;
			}
		}

		if ( ! $incompatible_plugins ) {
			return;
		}

		$message = str_replace(
			'<a>',
			'<a href="' . esc_url( add_query_arg( array( 'plugin_status' => 'incompatible_with_feature' ), admin_url( 'plugins.php' ) ) ) . '">',
			__( 'WooCommerce has detected that some of your active plugins are incompatible with currently enabled WooCommerce features. Please <a>review the details</a>.', 'woocommerce' )
		);

		// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
		?>
		<div class="notice notice-error">
		<p><?php echo $message; ?></p>
		</div>
		<?php
		// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Shows a "You are viewing the plugins that are incompatible with the X feature"
	 * if we are in the plugins page and the query string of the current request
	 * looks like '?plugin_status=incompatible_with_feature&feature_id=<feature id>'.
	 */
	private function maybe_display_current_feature_filter_description(): bool {
		if ( 'plugins' !== get_current_screen()->id ) {
			return false;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
		$plugin_status = $_GET['plugin_status'] ?? '';
		$feature_id    = $_GET['feature_id'] ?? '';
		// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput

		if ( 'incompatible_with_feature' !== $plugin_status ) {
			return false;
		}

		$feature_id = ( '' === $feature_id ) ? 'all' : $feature_id;

		if ( 'all' !== $feature_id && ! $this->feature_exists( $feature_id ) ) {
			return false;
		}

		// phpcs:enable WordPress.Security.NonceVerification
		$plugins_page_url  = admin_url( 'plugins.php' );
		$features_page_url = $this->get_features_page_url();

		$message =
			'all' === $feature_id
			? __( 'You are viewing active plugins that are incompatible with currently enabled WooCommerce features.', 'woocommerce' )
			: sprintf(
				/* translators: %s is a feature name. */
				__( "You are viewing the active plugins that are incompatible with the '%s' feature.", 'woocommerce' ),
				$this->features[ $feature_id ]['name']
			);

		$message .= '<br />';
		$message .= sprintf(
			__( "<a href='%1\$s'>View all plugins</a> - <a href='%2\$s'>Manage WooCommerce features</a>", 'woocommerce' ),
			$plugins_page_url,
			$features_page_url
		);

		// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
		?>
		<div class="notice notice-info">
			<p><?php echo $message; ?></p>
		</div>
		<?php
		// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped

		return true;
	}

	/**
	 * If the 'incompatible with features' plugin list is being rendered, invalidate existing cached plugin data.
	 *
	 * This heads off a problem in which WordPress's `get_plugins()` function may be called much earlier in the request
	 * (by third party code, for example), the results of which are cached, and before WooCommerce can modify the list
	 * to inject useful information of its own.
	 *
	 * @see https://github.com/woocommerce/woocommerce/issues/37343
	 *
	 * @return void
	 */
	private function maybe_invalidate_cached_plugin_data(): void {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		if ( ( $_GET['plugin_status'] ?? '' ) === 'incompatible_with_feature' ) {
			wp_cache_delete( 'plugins', 'plugins' );
		}
	}

	/**
	 * Handler for the 'after_plugin_row' action.
	 * Displays a "This plugin is incompatible with X features" notice if necessary.
	 *
	 * @param string $plugin_file The id of the plugin for which a row has been rendered in the plugins page.
	 * @param array  $plugin_data Plugin data, as returned by 'get_plugins'.
	 */
	private function handle_plugin_list_rows( $plugin_file, $plugin_data ) {
		global $wp_list_table;

		if ( 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			return;
		}

		if ( is_null( $wp_list_table ) || ! $this->plugin_util->is_woocommerce_aware_plugin( $plugin_data ) ) {
			return;
		}

		if ( ! $this->proxy->call_function( 'is_plugin_active', $plugin_file ) ) {
			return;
		}

		$feature_compatibility_info = $this->get_compatible_features_for_plugin( $plugin_file, true );
		$incompatible_features      = array_merge( $feature_compatibility_info['incompatible'], $feature_compatibility_info['uncertain'] );
		$incompatible_features      = array_values(
			array_filter(
				$incompatible_features,
				function( $feature_id ) {
					return ! $this->is_legacy_feature( $feature_id );
				}
			)
		);

		$incompatible_features_count = count( $incompatible_features );
		if ( $incompatible_features_count > 0 ) {
			$columns_count      = $wp_list_table->get_column_count();
			$is_active          = true; // For now we are showing active plugins in the "Incompatible with..." view.
			$is_active_class    = $is_active ? 'active' : 'inactive';
			$is_active_td_style = $is_active ? " style='border-left: 4px solid #72aee6;'" : '';

			if ( 1 === $incompatible_features_count ) {
				$message = sprintf(
					/* translators: %s = printable plugin name */
					__( "⚠ This plugin is incompatible with the enabled WooCommerce feature '%s', it shouldn't be activated.", 'woocommerce' ),
					$this->features[ $incompatible_features[0] ]['name']
				);
			} elseif ( 2 === $incompatible_features_count ) {
				/* translators: %1\$s, %2\$s = printable plugin names */
				$message = sprintf(
					__( "⚠ This plugin is incompatible with the enabled WooCommerce features '%1\$s' and '%2\$s', it shouldn't be activated.", 'woocommerce' ),
					$this->features[ $incompatible_features[0] ]['name'],
					$this->features[ $incompatible_features[1] ]['name']
				);
			} else {
				/* translators: %1\$s, %2\$s = printable plugin names, %3\$d = plugins count */
				$message = sprintf(
					__( "⚠ This plugin is incompatible with the enabled WooCommerce features '%1\$s', '%2\$s' and %3\$d more, it shouldn't be activated.", 'woocommerce' ),
					$this->features[ $incompatible_features[0] ]['name'],
					$this->features[ $incompatible_features[1] ]['name'],
					$incompatible_features_count - 2
				);
			}
			$features_page_url       = $this->get_features_page_url();
			$manage_features_message = __( 'Manage WooCommerce features', 'woocommerce' );

			// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
			?>
			<tr class='plugin-update-tr update <?php echo $is_active_class; ?>' data-plugin='<?php echo $plugin_file; ?>' data-plugin-row-type='feature-incomp-warn'>
				<td colspan='<?php echo $columns_count; ?>' class='plugin-update'<?php echo $is_active_td_style; ?>>
					<div class='notice inline notice-warning notice-alt'>
						<p>
							<?php echo $message; ?>
							<a href="<?php echo $features_page_url; ?>"><?php echo $manage_features_message; ?></a>
						</p>
					</div>
				</td>
			</tr>
			<?php
			// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
		}
	}

	/**
	 * Get the URL of the features settings page.
	 *
	 * @return string
	 */
	private function get_features_page_url(): string {
		return admin_url( 'admin.php?page=wc-settings&tab=advanced&section=features' );
	}

	/**
	 * Fix for the HTML of the plugins list when there are feature-plugin incompatibility warnings.
	 *
	 * WordPress renders the plugin information rows in the plugins page in <tr> elements as follows:
	 *
	 * - If the plugin needs update, the <tr> will have an "update" class. This will prevent the lower
	 *   border line to be drawn. Later an additional <tr> with an "update available" warning will be rendered,
	 *   it will have a "plugin-update-tr" class which will draw the missing lower border line.
	 * - Otherwise, the <tr> will be already drawn with the lower border line.
	 *
	 * This is a problem for our rendering of the "plugin is incompatible with X features" warning:
	 *
	 * - If the plugin info <tr> has "update", our <tr> will render nicely right after it; but then
	 *   our own "plugin-update-tr" class will draw an additional line before the "needs update" warning.
	 * - If not, the plugin info <tr> will render its lower border line right before our compatibility info <tr>.
	 *
	 * This small script fixes this by adding the "update" class to the plugin info <tr> if it doesn't have it
	 * (so no extra line before our <tr>), or removing 'plugin-update-tr' from our <tr> otherwise
	 * (and then some extra manual tweaking of margins is needed).
	 *
	 * @param string $current_screen The current screen object.
	 */
	private function enqueue_script_to_fix_plugin_list_html( $current_screen ): void {
		if ( 'plugins' !== $current_screen->id ) {
			return;
		}

		wc_enqueue_js(
			"
	    const warningRows = document.querySelectorAll('tr[data-plugin-row-type=\"feature-incomp-warn\"]');
	    for(const warningRow of warningRows) {
	    	const pluginName = warningRow.getAttribute('data-plugin');
			const pluginInfoRow = document.querySelector('tr.active[data-plugin=\"' + pluginName + '\"]:not(.plugin-update-tr), tr.inactive[data-plugin=\"' + pluginName + '\"]:not(.plugin-update-tr)');
			if(pluginInfoRow.classList.contains('update')) {
				warningRow.classList.remove('plugin-update-tr');
				warningRow.querySelector('.notice').style.margin = '5px 10px 15px 30px';
			}
			else {
				pluginInfoRow.classList.add('update');
			}
	    }
		"
		);
	}

	/**
	 * Handler for the 'views_plugins' hook that shows the links to the different views in the plugins page.
	 * If we come from a "Manage incompatible plugins" in the features page we'll show just two views:
	 * "All" (so that it's easy to go back to a known state) and "Incompatible with X".
	 * We'll skip the rest of the views since the counts are wrong anyway, as we are modifying
	 * the plugins list via the 'all_plugins' filter.
	 *
	 * @param array $views An array of view ids => view links.
	 * @return string[] The actual views array to use.
	 */
	private function handle_plugins_page_views_list( $views ): array {
		// phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
		if ( 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) {
			return $views;
		}

		$feature_id = $_GET['feature_id'] ?? 'all';
		if ( 'all' !== $feature_id && ! $this->feature_exists( $feature_id ) ) {
			return $views;
		}
		// phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput

		$all_items = get_plugins();

		$incompatible_plugins_count = count( $this->filter_plugins_list( $all_items ) );
		$incompatible_text          =
			'all' === $feature_id
			? __( 'Incompatible with WooCommerce features', 'woocommerce' )
			/* translators: %s = name of a WooCommerce feature */
			: sprintf( __( "Incompatible with '%s'", 'woocommerce' ), $this->features[ $feature_id ]['name'] );
		$incompatible_link = "<a href='plugins.php?plugin_status=incompatible_with_feature&feature_id={$feature_id}' class='current' aria-current='page'>{$incompatible_text} <span class='count'>({$incompatible_plugins_count})</span></a>";

		$all_plugins_count = count( $all_items );
		$all_text          = __( 'All', 'woocommerce' );
		$all_link          = "<a href='plugins.php?plugin_status=all'>{$all_text} <span class='count'>({$all_plugins_count})</span></a>";

		return array(
			'all'                       => $all_link,
			'incompatible_with_feature' => $incompatible_link,
		);
	}

	/**
	 * Set the feature nonce to be sent from client side.
	 *
	 * @param array $settings Component settings.
	 *
	 * @return array
	 */
	public function set_change_feature_enable_nonce( $settings ) {
		$settings['_feature_nonce'] = wp_create_nonce( 'change_feature_enable' );
		return $settings;
	}

	/**
	 * Changes the feature given it's id, a toggle value and nonce as a query param.
	 *
	 * `/wp-admin/post.php?product_block_editor=1&_feature_nonce=1234`, 1 for on
	 * `/wp-admin/post.php?product_block_editor=0&_feature_nonce=1234`, 0 for off
	 */
	private function change_feature_enable_from_query_params(): void {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			return;
		}

		$is_feature_nonce_invalid = ( ! isset( $_GET['_feature_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_feature_nonce'] ) ), 'change_feature_enable' ) );

		$query_params_to_remove = array( '_feature_nonce' );

		foreach ( array_keys( $this->features ) as $feature_id ) {
			if ( isset( $_GET[ $feature_id ] ) && is_numeric( $_GET[ $feature_id ] ) ) {
				$value = absint( $_GET[ $feature_id ] );

				if ( $is_feature_nonce_invalid ) {
					wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) );
					return;
				}

				if ( 1 === $value ) {
					$this->change_feature_enable( $feature_id, true );
				} elseif ( 0 === $value ) {
					$this->change_feature_enable( $feature_id, false );
				}
				$query_params_to_remove[] = $feature_id;
			}
		}
		if ( count( $query_params_to_remove ) > 1 && isset( $_SERVER['REQUEST_URI'] ) ) {
			// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			wp_safe_redirect( remove_query_arg( $query_params_to_remove, $_SERVER['REQUEST_URI'] ) );
		}
	}
}