rebase on oct-10-2023
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Block template registry.
|
||||
*/
|
||||
final class BlockTemplateRegistry {
|
||||
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var BlockTemplateRegistry|null
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Templates.
|
||||
*/
|
||||
protected $templates = array();
|
||||
|
||||
/**
|
||||
* Get the instance of the class.
|
||||
*/
|
||||
public static function get_instance(): BlockTemplateRegistry {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a single template.
|
||||
*
|
||||
* @param string $id Template ID.
|
||||
* @param array $template Template layout.
|
||||
*/
|
||||
public function register( BlockTemplateInterface $template ) {
|
||||
$id = $template->get_id();
|
||||
|
||||
if ( isset( $this->templates[ $id ] ) ) {
|
||||
throw new \ValueError( 'A template with the specified ID already exists in the registry.' );
|
||||
}
|
||||
|
||||
$this->templates[ $id ] = $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the registered templates.
|
||||
*/
|
||||
public function get_all_registered(): array {
|
||||
return $this->templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single registered template.
|
||||
*
|
||||
* @param string $id ID of the template
|
||||
*/
|
||||
public function get_registered( $id ): BlockTemplateInterface {
|
||||
return isset( $this->templates[ $id ] ) ? $this->templates[ $id ] : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry;
|
||||
|
||||
/**
|
||||
* Block template controller.
|
||||
*/
|
||||
class BlockTemplatesController {
|
||||
|
||||
/**
|
||||
* Block template registry
|
||||
*
|
||||
* @var BlockTemplateRegistry
|
||||
*/
|
||||
private $block_template_registry;
|
||||
|
||||
/**
|
||||
* Block template transformer.
|
||||
*
|
||||
* @var TemplateTransformer
|
||||
*/
|
||||
private $template_transformer;
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*/
|
||||
public function init( $block_template_registry, $template_transformer ) {
|
||||
$this->block_template_registry = $block_template_registry;
|
||||
$this->template_transformer = $template_transformer;
|
||||
add_action( 'rest_api_init', array( $this, 'register_templates' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register templates in the blocks endpoint.
|
||||
*/
|
||||
public function register_templates() {
|
||||
$templates = $this->block_template_registry->get_all_registered();
|
||||
|
||||
foreach ( $templates as $template ) {
|
||||
add_filter( 'pre_get_block_templates', function( $query_result, $query, $template_type ) use( $template ) {
|
||||
if ( ! isset( $query['area'] ) || $query['area'] !== $template->get_area() ) {
|
||||
return $query_result;
|
||||
}
|
||||
|
||||
$wp_block_template = $this->template_transformer->transform( $template );
|
||||
$query_result[] = $wp_block_template;
|
||||
|
||||
return $query_result;
|
||||
}, 10, 3 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Template transformer.
|
||||
*/
|
||||
class TemplateTransformer {
|
||||
|
||||
/**
|
||||
* Transform the WooCommerceBlockTemplate to a WP_Block_Template.
|
||||
*
|
||||
* @param object $block_template The product template.
|
||||
*/
|
||||
public function transform( BlockTemplateInterface $block_template ): \WP_Block_Template {
|
||||
$template = new \WP_Block_Template();
|
||||
$template->id = $block_template->get_id();
|
||||
$template->theme = 'woocommerce/woocommerce';
|
||||
$template->content = $block_template->get_formatted_template();
|
||||
$template->source = 'plugin';
|
||||
$template->slug = $block_template->get_id();
|
||||
$template->type = 'wp_template';
|
||||
$template->title = $block_template->get_title();
|
||||
$template->description = $block_template->get_description();
|
||||
$template->status = 'publish';
|
||||
$template->has_theme_file = true;
|
||||
$template->origin = 'plugin';
|
||||
$template->is_custom = false; // Templates loaded from the filesystem aren't custom, ones that have been edited and loaded from the DB are.
|
||||
$template->post_types = array(); // Don't appear in any Edit Post template selector dropdown.
|
||||
$template->area = $block_template->get_area();
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Block configuration used to specify blocks in BlockTemplate.
|
||||
*/
|
||||
class AbstractBlock implements BlockInterface {
|
||||
/**
|
||||
* The block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* The block ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* The block order.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $order = 10;
|
||||
|
||||
/**
|
||||
* The block attributes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $attributes = [];
|
||||
|
||||
/**
|
||||
* The block template that this block belongs to.
|
||||
*
|
||||
* @var BlockTemplate
|
||||
*/
|
||||
private $root_template;
|
||||
|
||||
/**
|
||||
* The parent container.
|
||||
*
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
private $parent;
|
||||
|
||||
/**
|
||||
* Block constructor.
|
||||
*
|
||||
* @param array $config The block configuration.
|
||||
* @param BlockTemplateInterface $root_template The block template that this block belongs to.
|
||||
* @param BlockContainerInterface|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.
|
||||
*/
|
||||
public function __construct( array $config, BlockTemplateInterface &$root_template, ContainerInterface &$parent = null ) {
|
||||
$this->validate( $config, $root_template, $parent );
|
||||
|
||||
$this->root_template = $root_template;
|
||||
$this->parent = is_null( $parent ) ? $root_template : $parent;
|
||||
|
||||
$this->name = $config[ self::NAME_KEY ];
|
||||
|
||||
if ( ! isset( $config[ self::ID_KEY ] ) ) {
|
||||
$this->id = $this->root_template->generate_block_id( $this->get_name() );
|
||||
} else {
|
||||
$this->id = $config[ self::ID_KEY ];
|
||||
}
|
||||
|
||||
if ( isset( $config[ self::ORDER_KEY ] ) ) {
|
||||
$this->order = $config[ self::ORDER_KEY ];
|
||||
}
|
||||
|
||||
if ( isset( $config[ self::ATTRIBUTES_KEY ] ) ) {
|
||||
$this->attributes = $config[ self::ATTRIBUTES_KEY ];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate block configuration.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
protected function validate( array $config, BlockTemplateInterface &$root_template, ContainerInterface &$parent = null ) {
|
||||
if ( isset( $parent ) && ( $parent->get_root_template() !== $root_template ) ) {
|
||||
throw new \ValueError( 'The parent block must belong to the same template as the block.' );
|
||||
}
|
||||
|
||||
if ( ! isset( $config[ self::NAME_KEY ] ) || ! is_string( $config[ self::NAME_KEY ] ) ) {
|
||||
throw new \ValueError( 'The block name must be specified.' );
|
||||
}
|
||||
|
||||
if ( isset( $config[ self::ORDER_KEY ] ) && ! is_int( $config[ self::ORDER_KEY ] ) ) {
|
||||
throw new \ValueError( 'The block order must be an integer.' );
|
||||
}
|
||||
|
||||
if ( isset( $config[ self::ATTRIBUTES_KEY ] ) && ! is_array( $config[ self::ATTRIBUTES_KEY ] ) ) {
|
||||
throw new \ValueError( 'The block attributes must be an array.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block name.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block ID.
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block order.
|
||||
*/
|
||||
public function get_order(): int {
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the block order.
|
||||
*
|
||||
* @param int $order The block order.
|
||||
*/
|
||||
public function set_order( int $order ) {
|
||||
$this->order = $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block attributes.
|
||||
*/
|
||||
public function get_attributes(): array {
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the block attributes.
|
||||
*
|
||||
* @param array $attributes The block attributes.
|
||||
*/
|
||||
public function set_attributes( array $attributes ) {
|
||||
$this->attributes = $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template that this block belongs to.
|
||||
*/
|
||||
public function &get_root_template(): BlockTemplateInterface {
|
||||
return $this->root_template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent block container.
|
||||
*/
|
||||
public function &get_parent(): ContainerInterface {
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block configuration as a formatted template.
|
||||
*
|
||||
* @return array The block configuration as a formatted template.
|
||||
*/
|
||||
public function get_formatted_template(): array {
|
||||
$arr = [
|
||||
$this->get_name(),
|
||||
$this->get_attributes(),
|
||||
];
|
||||
|
||||
return $arr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Block template class.
|
||||
*/
|
||||
abstract class AbstractBlockTemplate implements BlockTemplateInterface {
|
||||
use BlockContainerTrait;
|
||||
|
||||
/**
|
||||
* Get the template ID.
|
||||
*/
|
||||
public abstract function get_id(): string;
|
||||
|
||||
/**
|
||||
* Get the template title.
|
||||
*/
|
||||
public function get_title(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template description.
|
||||
*/
|
||||
public function get_description(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template area.
|
||||
*/
|
||||
public function get_area(): string {
|
||||
return 'uncategorized';
|
||||
}
|
||||
|
||||
/**
|
||||
* The block cache.
|
||||
*
|
||||
* @var BlockInterface[]
|
||||
*/
|
||||
private $block_cache = [];
|
||||
|
||||
/**
|
||||
* Get a block by ID.
|
||||
*
|
||||
* @param string $block_id The block ID.
|
||||
*/
|
||||
public function get_block( string $block_id ): ?BlockInterface {
|
||||
return $this->block_cache[ $block_id ] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches a block in the template. This is an internal method and should not be called directly
|
||||
* except for classes that implement BlockContainerInterface, in their add_block() method.
|
||||
*
|
||||
* @param BlockInterface $block The block to cache.
|
||||
*
|
||||
* @throws \ValueError If a block with the specified ID already exists in the template.
|
||||
* @throws \ValueError If the block template that the block belongs to is not this template.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
public function cache_block( BlockInterface &$block ) {
|
||||
$id = $block->get_id();
|
||||
|
||||
if ( isset( $this->block_cache[ $id ] ) ) {
|
||||
throw new \ValueError( 'A block with the specified ID already exists in the template.' );
|
||||
}
|
||||
|
||||
if ( $block->get_root_template() !== $this ) {
|
||||
throw new \ValueError( 'The block template that the block belongs to must be the same as this template.' );
|
||||
}
|
||||
|
||||
$this->block_cache[ $id ] = $block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a block ID based on a base.
|
||||
*
|
||||
* @param string $id_base The base to use when generating an ID.
|
||||
* @return string
|
||||
*/
|
||||
public function generate_block_id( string $id_base ): string {
|
||||
$instance_count = 0;
|
||||
|
||||
do {
|
||||
$instance_count++;
|
||||
$block_id = $id_base . '-' . $instance_count;
|
||||
} while ( isset( $this->block_cache[ $block_id ] ) );
|
||||
|
||||
return $block_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the root template.
|
||||
*/
|
||||
public function &get_root_template(): BlockTemplateInterface {
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inner blocks as a formatted template.
|
||||
*/
|
||||
public function get_formatted_template(): array {
|
||||
$inner_blocks = $this->get_inner_blocks_sorted_by_order();
|
||||
|
||||
$inner_blocks_formatted_template = array_map(
|
||||
function( Block $block ) {
|
||||
return $block->get_formatted_template();
|
||||
},
|
||||
$inner_blocks
|
||||
);
|
||||
|
||||
return $inner_blocks_formatted_template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockContainerInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Generic block with container properties to be used in BlockTemplate.
|
||||
*/
|
||||
class Block extends AbstractBlock implements BlockContainerInterface {
|
||||
use BlockContainerTrait;
|
||||
|
||||
/**
|
||||
* Get the block configuration as a formatted template.
|
||||
*
|
||||
* @return array The block configuration as a formatted template.
|
||||
*/
|
||||
public function get_formatted_template(): array {
|
||||
$arr = [
|
||||
$this->get_name(),
|
||||
$this->get_attributes(),
|
||||
];
|
||||
|
||||
$inner_blocks = $this->get_inner_blocks_sorted_by_order();
|
||||
|
||||
if ( ! empty( $inner_blocks ) ) {
|
||||
$arr[] = array_map(
|
||||
function( BlockInterface $block ) {
|
||||
return $block->get_formatted_template();
|
||||
},
|
||||
$inner_blocks
|
||||
);
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an inner block to this block.
|
||||
*
|
||||
* @param array $block_config The block data.
|
||||
*/
|
||||
public function &add_block( array $block_config ): BlockInterface {
|
||||
$block = new Block( $block_config, $this->get_root_template(), $this );
|
||||
return $this->add_inner_block( $block );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
|
||||
/**
|
||||
* Trait for block containers.
|
||||
*/
|
||||
trait BlockContainerTrait {
|
||||
/**
|
||||
* The inner blocks.
|
||||
*
|
||||
* @var BlockInterface[]
|
||||
*/
|
||||
private $inner_blocks = [];
|
||||
|
||||
// phpcs doesn't take into account exceptions thrown by called methods.
|
||||
// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
|
||||
/**
|
||||
* Add a block to the block container.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
*
|
||||
* @throws \ValueError If the block configuration is invalid.
|
||||
* @throws \ValueError If a block with the specified ID already exists in the template.
|
||||
* @throws \UnexpectedValueException If the block container is not the parent of the block.
|
||||
*/
|
||||
protected function &add_inner_block( BlockInterface $block ): BlockInterface {
|
||||
if ( ! $block instanceof BlockInterface ) {
|
||||
throw new \UnexpectedValueException( 'The block must return an instance of BlockInterface.' );
|
||||
}
|
||||
|
||||
if ( $block->get_parent() !== $this ) {
|
||||
throw new \UnexpectedValueException( 'The block container is not the parent of the block.' );
|
||||
}
|
||||
|
||||
$root_template = $block->get_root_template();
|
||||
$root_template->cache_block( $block );
|
||||
$this->inner_blocks[] = &$block;
|
||||
return $block;
|
||||
}
|
||||
|
||||
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
|
||||
/**
|
||||
* Get the inner blocks sorted by order.
|
||||
*/
|
||||
private function get_inner_blocks_sorted_by_order(): array {
|
||||
$sorted_inner_blocks = $this->inner_blocks;
|
||||
|
||||
usort(
|
||||
$sorted_inner_blocks,
|
||||
function( Block $a, Block $b ) {
|
||||
return $a->get_order() <=> $b->get_order();
|
||||
}
|
||||
);
|
||||
|
||||
return $sorted_inner_blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inner blocks as a formatted template.
|
||||
*/
|
||||
public function get_formatted_template(): array {
|
||||
$arr = [
|
||||
$this->get_name(),
|
||||
$this->get_attributes(),
|
||||
];
|
||||
|
||||
$inner_blocks = $this->get_inner_blocks_sorted_by_order();
|
||||
|
||||
if ( ! empty( $inner_blocks ) ) {
|
||||
$arr[] = array_map(
|
||||
function( BlockInterface $block ) {
|
||||
return $block->get_formatted_template();
|
||||
},
|
||||
$inner_blocks
|
||||
);
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Block template class.
|
||||
*/
|
||||
class BlockTemplate extends AbstractBlockTemplate {
|
||||
/**
|
||||
* Get the template ID.
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return 'woocommerce-block-template';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a block ID based on a base.
|
||||
*
|
||||
* @param array $block_config The block data.
|
||||
*/
|
||||
public function add_block( array $block_config ): BlockInterface {
|
||||
$block = new Block( $block_config, $this->get_root_template(), $this );
|
||||
return $this->add_inner_block( $block );
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ namespace Automattic\WooCommerce\Internal\Admin;
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplatesController;
|
||||
use Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews;
|
||||
use Automattic\WooCommerce\Internal\Admin\ProductReviews\ReviewsCommentsOverrides;
|
||||
use Automattic\WooCommerce\Internal\Admin\Settings;
|
||||
@@ -72,6 +73,7 @@ class Loader {
|
||||
|
||||
wc_get_container()->get( Reviews::class );
|
||||
wc_get_container()->get( ReviewsCommentsOverrides::class );
|
||||
wc_get_container()->get( BlockTemplatesController::class );
|
||||
|
||||
add_filter( 'admin_body_class', array( __CLASS__, 'add_admin_body_classes' ) );
|
||||
add_filter( 'admin_title', array( __CLASS__, 'update_admin_title' ) );
|
||||
@@ -302,7 +304,7 @@ class Loader {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks extra neccessary data into the component settings array already set in WooCommerce core.
|
||||
* Hooks extra necessary data into the component settings array already set in WooCommerce core.
|
||||
*
|
||||
* @param array $settings Array of component settings.
|
||||
* @return array Array of component settings.
|
||||
|
||||
@@ -52,7 +52,7 @@ class MarketingSpecs {
|
||||
|
||||
if ( false === $plugins ) {
|
||||
$request = wp_remote_get(
|
||||
'https://woocommerce.com/wp-json/wccom/marketing-tab/1.2/recommendations.json',
|
||||
'https://woocommerce.com/wp-json/wccom/marketing-tab/1.3/recommendations.json',
|
||||
array(
|
||||
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
|
||||
)
|
||||
@@ -136,34 +136,41 @@ class MarketingSpecs {
|
||||
/**
|
||||
* Load knowledge base posts from WooCommerce.com
|
||||
*
|
||||
* @param string|null $category Category of posts to retrieve.
|
||||
* @param string|null $term Term of posts to retrieve.
|
||||
* @return array
|
||||
*/
|
||||
public function get_knowledge_base_posts( ?string $category ): array {
|
||||
$kb_transient = self::KNOWLEDGE_BASE_TRANSIENT;
|
||||
|
||||
$categories = array(
|
||||
'marketing' => 1744,
|
||||
'coupons' => 25202,
|
||||
public function get_knowledge_base_posts( ?string $term ): array {
|
||||
$terms = array(
|
||||
'marketing' => array(
|
||||
'taxonomy' => 'category',
|
||||
'term_id' => 1744,
|
||||
'argument' => 'categories',
|
||||
),
|
||||
'coupons' => array(
|
||||
'taxonomy' => 'post_tag',
|
||||
'term_id' => 1377,
|
||||
'argument' => 'tags',
|
||||
),
|
||||
);
|
||||
|
||||
// Default to marketing category (if no category set on the kb component).
|
||||
if ( ! empty( $category ) && array_key_exists( $category, $categories ) ) {
|
||||
$category_id = $categories[ $category ];
|
||||
$kb_transient = $kb_transient . '_' . strtolower( $category );
|
||||
} else {
|
||||
$category_id = $categories['marketing'];
|
||||
// Default to the marketing category (if no term is set on the kb component).
|
||||
if ( empty( $term ) || ! array_key_exists( $term, $terms ) ) {
|
||||
$term = 'marketing';
|
||||
}
|
||||
|
||||
$term_id = $terms[ $term ]['term_id'];
|
||||
$argument = $terms[ $term ]['argument'];
|
||||
$kb_transient = self::KNOWLEDGE_BASE_TRANSIENT . '_' . strtolower( $term );
|
||||
|
||||
$posts = get_transient( $kb_transient );
|
||||
|
||||
if ( false === $posts ) {
|
||||
$request_url = add_query_arg(
|
||||
array(
|
||||
'categories' => $category_id,
|
||||
'page' => 1,
|
||||
'per_page' => 8,
|
||||
'_embed' => 1,
|
||||
$argument => $term_id,
|
||||
'page' => 1,
|
||||
'per_page' => 8,
|
||||
'_embed' => 1,
|
||||
),
|
||||
'https://woocommerce.com/wp-json/wp/v2/posts?utm_medium=product'
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ class CouponPageMoved {
|
||||
|
||||
add_action( 'admin_init', [ $this, 'possibly_add_note' ] );
|
||||
add_action( 'admin_init', [ $this, 'redirect_to_coupons' ] );
|
||||
add_action( 'woocommerce_admin_newly_installed', [ $this, 'disable_legacy_menu_for_new_install' ] );
|
||||
add_action( 'woocommerce_newly_installed', [ $this, 'disable_legacy_menu_for_new_install' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Internal\Admin\WcPayWelcomePage;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
@@ -38,19 +38,13 @@ class PaymentsMoreInfoNeeded {
|
||||
* @return bool
|
||||
*/
|
||||
public static function should_display_note() {
|
||||
// If user has installed WCPay, don't show this note.
|
||||
$installed_plugins = PluginsHelper::get_installed_plugin_slugs();
|
||||
if ( in_array( 'woocommerce-payments', $installed_plugins, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// User has dismissed the WCPay Welcome Page.
|
||||
if ( 'yes' !== get_option( 'wc_calypso_bridge_payments_dismissed', 'no' ) ) {
|
||||
// WCPay welcome page must not be visible.
|
||||
if ( WcPayWelcomePage::instance()->must_be_visible() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// More than 30 days since viewing the welcome page.
|
||||
$exit_survey_timestamp = get_option( 'wc_pay_exit_survey_more_info_needed_timestamp', false );
|
||||
$exit_survey_timestamp = get_option( 'wcpay_welcome_page_exit_survey_more_info_needed_timestamp', false );
|
||||
if ( ! $exit_survey_timestamp ||
|
||||
( time() - $exit_survey_timestamp < 30 * DAY_IN_SECONDS )
|
||||
) {
|
||||
@@ -69,10 +63,10 @@ class PaymentsMoreInfoNeeded {
|
||||
if ( ! self::should_display_note() ) {
|
||||
return;
|
||||
}
|
||||
$content = __( 'We recently asked you if you wanted more information about WooCommerce Payments. Run your business and manage your payments in one place with the solution built and supported by WooCommerce.', 'woocommerce' );
|
||||
$content = __( 'We recently asked you if you wanted more information about WooPayments. Run your business and manage your payments in one place with the solution built and supported by WooCommerce.', 'woocommerce' );
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Payments made simple with WooCommerce Payments', 'woocommerce' ) );
|
||||
$note->set_title( __( 'Payments made simple with WooPayments', 'woocommerce' ) );
|
||||
$note->set_content( $content );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Internal\Admin\WcPayWelcomePage;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
@@ -38,18 +38,13 @@ class PaymentsRemindMeLater {
|
||||
* @return bool
|
||||
*/
|
||||
public static function should_display_note() {
|
||||
// Installed WCPay.
|
||||
$installed_plugins = PluginsHelper::get_installed_plugin_slugs();
|
||||
if ( in_array( 'woocommerce-payments', $installed_plugins, true ) ) {
|
||||
return false;
|
||||
}
|
||||
// Dismissed WCPay welcome page.
|
||||
if ( 'yes' === get_option( 'wc_calypso_bridge_payments_dismissed', 'no' ) ) {
|
||||
// WCPay welcome page must be visible.
|
||||
if ( ! WcPayWelcomePage::instance()->must_be_visible() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Less than 3 days since viewing welcome page.
|
||||
$view_timestamp = get_option( 'wc_pay_welcome_page_viewed_timestamp', false );
|
||||
$view_timestamp = get_option( 'wcpay_welcome_page_viewed_timestamp', false );
|
||||
if ( ! $view_timestamp ||
|
||||
( time() - $view_timestamp < 3 * DAY_IN_SECONDS )
|
||||
) {
|
||||
@@ -68,10 +63,10 @@ class PaymentsRemindMeLater {
|
||||
if ( ! self::should_display_note() ) {
|
||||
return;
|
||||
}
|
||||
$content = __( 'Save up to $800 in fees by managing transactions with WooCommerce Payments. With WooCommerce Payments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies.', 'woocommerce' );
|
||||
$content = __( 'Save up to $800 in fees by managing transactions with WooPayments. With WooPayments, you can securely accept major cards, Apple Pay, and payments in over 100 currencies.', 'woocommerce' );
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Save big with WooCommerce Payments', 'woocommerce' ) );
|
||||
$note->set_title( __( 'Save big with WooPayments', 'woocommerce' ) );
|
||||
$note->set_content( $content );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
|
||||
@@ -105,7 +105,7 @@ class WooCommercePayments {
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Try the new way to get paid', 'woocommerce' ) );
|
||||
$note->set_content(
|
||||
__( 'Securely accept credit and debit cards on your site. Manage transactions without leaving your WordPress dashboard. Only with <strong>WooCommerce Payments</strong>.', 'woocommerce' ) .
|
||||
__( 'Securely accept credit and debit cards on your site. Manage transactions without leaving your WordPress dashboard. Only with <strong>WooPayments</strong>.', 'woocommerce' ) .
|
||||
'<br><br>' .
|
||||
sprintf(
|
||||
/* translators: 1: opening link tag, 2: closing tag */
|
||||
|
||||
@@ -11,6 +11,7 @@ defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\Notes;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
|
||||
/**
|
||||
* Woo_Subscriptions_Notes
|
||||
@@ -36,7 +37,7 @@ class WooSubscriptionsNotes {
|
||||
* Hook all the things.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'admin_init', array( $this, 'admin_init' ) );
|
||||
add_action( 'admin_head', array( $this, 'admin_head' ) );
|
||||
add_action( 'update_option_woocommerce_helper_data', array( $this, 'update_option_woocommerce_helper_data' ), 10, 2 );
|
||||
}
|
||||
|
||||
@@ -75,9 +76,16 @@ class WooSubscriptionsNotes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Things to do on admin_init.
|
||||
* Runs on `admin_head` hook. Checks the connection and refreshes subscription notes on relevant pages.
|
||||
*/
|
||||
public function admin_init() {
|
||||
public function admin_head() {
|
||||
if ( ! PageController::is_admin_or_embed_page() ) {
|
||||
// To avoid unnecessarily calling Helper API, we only want to refresh subscription notes,
|
||||
// if the request is initiated from the wc admin dashboard or a WC related page which includes
|
||||
// the Activity button in WC header.
|
||||
return;
|
||||
}
|
||||
|
||||
$this->check_connection();
|
||||
|
||||
if ( $this->is_connected() ) {
|
||||
|
||||
@@ -128,9 +128,6 @@ class OnboardingProducts {
|
||||
$product_data[ $key ]['description'] = $products[ $product_type['product'] ]->excerpt;
|
||||
$product_data[ $key ]['more_url'] = $products[ $product_type['product'] ]->link;
|
||||
$product_data[ $key ]['slug'] = strtolower( preg_replace( '~[^\pL\d]+~u', '-', $products[ $product_type['product'] ]->slug ) );
|
||||
} elseif ( isset( $product_type['product'] ) ) {
|
||||
/* translators: site currency symbol (used to show that the product costs money) */
|
||||
$product_data[ $key ]['label'] .= sprintf( __( ' — %s', 'woocommerce' ), html_entity_decode( get_woocommerce_currency_symbol() ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ class OnboardingProfile {
|
||||
* Add onboarding actions.
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'woocommerce_updated', array( __CLASS__, 'maybe_mark_complete' ) );
|
||||
add_action( 'update_option_' . self::DATA_OPTION, array( __CLASS__, 'trigger_complete' ), 10, 2 );
|
||||
}
|
||||
|
||||
@@ -65,37 +64,4 @@ class OnboardingProfile {
|
||||
// https://github.com/woocommerce/woocommerce-admin/pull/2300#discussion_r287237498.
|
||||
return ! $is_completed && ! $is_skipped;
|
||||
}
|
||||
|
||||
/**
|
||||
* When updating WooCommerce, mark the profiler and task list complete.
|
||||
*
|
||||
* @todo The `maybe_enable_setup_wizard()` method should be revamped on onboarding enable in core.
|
||||
* See https://github.com/woocommerce/woocommerce/blob/1ca791f8f2325fe2ee0947b9c47e6a4627366374/includes/class-wc-install.php#L341
|
||||
*/
|
||||
public static function maybe_mark_complete() {
|
||||
// The install notice still exists so don't complete the profiler.
|
||||
if ( ! class_exists( 'WC_Admin_Notices' ) || \WC_Admin_Notices::has_notice( 'install' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$onboarding_data = get_option( self::DATA_OPTION, array() );
|
||||
// Don't make updates if the profiler is completed or skipped, but task list is potentially incomplete.
|
||||
if (
|
||||
( isset( $onboarding_data['completed'] ) && $onboarding_data['completed'] ) ||
|
||||
( isset( $onboarding_data['skipped'] ) && $onboarding_data['skipped'] )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$onboarding_data['completed'] = true;
|
||||
update_option( self::DATA_OPTION, $onboarding_data );
|
||||
|
||||
if ( ! WCAdminHelper::is_wc_admin_active_for( DAY_IN_SECONDS ) ) {
|
||||
$task_list = TaskLists::get_list( 'setup' );
|
||||
if ( ! $task_list ) {
|
||||
return;
|
||||
}
|
||||
$task_list->hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +58,10 @@ class COTRedirectionController {
|
||||
$params['_wpnonce'] = wp_create_nonce( 'bulk-posts' );
|
||||
}
|
||||
|
||||
// If an `order` array parameter is present, rename as `post`.
|
||||
if ( isset( $params['order'] ) && is_array( $params['order'] ) ) {
|
||||
$params['post'] = $params['order'];
|
||||
unset( $params['order'] );
|
||||
// If an `id` array parameter is present, rename as `post`.
|
||||
if ( isset( $params['id'] ) && is_array( $params['id'] ) ) {
|
||||
$params['post'] = $params['id'];
|
||||
unset( $params['id'] );
|
||||
}
|
||||
|
||||
$params['post_type'] = 'shop_order';
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\TaxonomiesMetaBox;
|
||||
|
||||
/**
|
||||
* Class Edit.
|
||||
@@ -26,6 +27,13 @@ class Edit {
|
||||
*/
|
||||
private $custom_meta_box;
|
||||
|
||||
/**
|
||||
* Instance of the TaxonomiesMetaBox class. Used to render meta box for taxonomies.
|
||||
*
|
||||
* @var TaxonomiesMetaBox
|
||||
*/
|
||||
private $taxonomies_meta_box;
|
||||
|
||||
/**
|
||||
* Instance of WC_Order to be used in metaboxes.
|
||||
*
|
||||
@@ -47,6 +55,13 @@ class Edit {
|
||||
*/
|
||||
private $message;
|
||||
|
||||
/**
|
||||
* Controller for orders page. Used to determine redirection URLs.
|
||||
*
|
||||
* @var PageController
|
||||
*/
|
||||
private $orders_page_controller;
|
||||
|
||||
/**
|
||||
* Hooks all meta-boxes for order edit page. This is static since this may be called by post edit form rendering.
|
||||
*
|
||||
@@ -96,6 +111,20 @@ class Edit {
|
||||
wp_enqueue_script( 'post' ); // Ensure existing JS libraries are still available for backward compat.
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PageController for this edit form. This method is protected to allow child classes to overwrite the PageController object and return custom links.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*
|
||||
* @return PageController PageController object.
|
||||
*/
|
||||
protected function get_page_controller() {
|
||||
if ( ! isset( $this->orders_page_controller ) ) {
|
||||
$this->orders_page_controller = wc_get_container()->get( PageController::class );
|
||||
}
|
||||
return $this->orders_page_controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup hooks, actions and variables needed to render order edit page.
|
||||
*
|
||||
@@ -103,17 +132,22 @@ class Edit {
|
||||
*/
|
||||
public function setup( \WC_Order $order ) {
|
||||
$this->order = $order;
|
||||
$wc_screen_id = wc_get_page_screen_id( 'shop-order' );
|
||||
$current_screen = get_current_screen();
|
||||
$current_screen->is_block_editor( false );
|
||||
$this->screen_id = $current_screen->id;
|
||||
if ( ! isset( $this->custom_meta_box ) ) {
|
||||
$this->custom_meta_box = wc_get_container()->get( CustomMetaBox::class );
|
||||
}
|
||||
|
||||
if ( ! isset( $this->taxonomies_meta_box ) ) {
|
||||
$this->taxonomies_meta_box = wc_get_container()->get( TaxonomiesMetaBox::class );
|
||||
}
|
||||
|
||||
$this->add_save_meta_boxes();
|
||||
$this->handle_order_update();
|
||||
$this->add_order_meta_boxes( $this->screen_id, __( 'Order', 'woocommerce' ) );
|
||||
$this->add_order_specific_meta_box();
|
||||
$this->add_order_taxonomies_meta_box();
|
||||
|
||||
/**
|
||||
* From wp-admin/includes/meta-boxes.php.
|
||||
@@ -122,7 +156,7 @@ class Edit {
|
||||
*
|
||||
* @since 3.8.0.
|
||||
*/
|
||||
do_action( 'add_meta_boxes', $wc_screen_id, $this->order );
|
||||
do_action( 'add_meta_boxes', $this->screen_id, $this->order );
|
||||
|
||||
/**
|
||||
* Provides an opportunity to inject custom meta boxes into the order editor screen. This
|
||||
@@ -132,7 +166,7 @@ class Edit {
|
||||
*
|
||||
* @oaram WC_Order $order The order being edited.
|
||||
*/
|
||||
do_action( 'add_meta_boxes_' . $wc_screen_id, $this->order );
|
||||
do_action( 'add_meta_boxes_' . $this->screen_id, $this->order );
|
||||
|
||||
$this->enqueue_scripts();
|
||||
}
|
||||
@@ -159,6 +193,15 @@ class Edit {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render custom meta box.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function add_order_taxonomies_meta_box() {
|
||||
$this->taxonomies_meta_box->add_taxonomies_meta_boxes( $this->screen_id, $this->order->get_type() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes care of updating order data. Fires action that metaboxes can hook to for order data updating.
|
||||
*
|
||||
@@ -176,6 +219,10 @@ class Edit {
|
||||
|
||||
check_admin_referer( $this->get_order_edit_nonce_action() );
|
||||
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized later on by taxonomies_meta_box object.
|
||||
$taxonomy_input = isset( $_POST['tax_input'] ) ? wp_unslash( $_POST['tax_input'] ) : null;
|
||||
$this->taxonomies_meta_box->save_taxonomies( $this->order, $taxonomy_input );
|
||||
|
||||
/**
|
||||
* Save meta for shop order.
|
||||
*
|
||||
@@ -189,9 +236,39 @@ class Edit {
|
||||
// Order updated message.
|
||||
$this->message = 1;
|
||||
|
||||
// Refresh the order from DB.
|
||||
$this->order = wc_get_order( $this->order->get_id() );
|
||||
$theorder = $this->order;
|
||||
$this->redirect_order( $this->order );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to redirect to order edit page.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
*/
|
||||
private function redirect_order( \WC_Order $order ) {
|
||||
$redirect_to = $this->get_page_controller()->get_edit_url( $order->get_id() );
|
||||
if ( isset( $this->message ) ) {
|
||||
$redirect_to = add_query_arg( 'message', $this->message, $redirect_to );
|
||||
}
|
||||
wp_safe_redirect(
|
||||
/**
|
||||
* Filter the URL used to redirect after an order is updated. Similar to the WP post's `redirect_post_location` filter.
|
||||
*
|
||||
* @param string $redirect_to The redirect destination URL.
|
||||
* @param int $order_id The order ID.
|
||||
* @param \WC_Order $order The order object.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*/
|
||||
apply_filters(
|
||||
'woocommerce_redirect_order_location',
|
||||
$redirect_to,
|
||||
$order->get_id(),
|
||||
$order
|
||||
)
|
||||
);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,10 +330,10 @@ class Edit {
|
||||
private function render_wrapper_start( $notice = '', $message = '' ) {
|
||||
$post_type = get_post_type_object( $this->order->get_type() );
|
||||
|
||||
$edit_page_url = wc_get_container()->get( PageController::class )->get_edit_url( $this->order->get_id() );
|
||||
$edit_page_url = $this->get_page_controller()->get_edit_url( $this->order->get_id() );
|
||||
$form_action = 'edit_order';
|
||||
$referer = wp_get_referer();
|
||||
$new_page_url = wc_get_container()->get( PageController::class )->get_new_page_url( $this->order->get_type() );
|
||||
$new_page_url = $this->get_page_controller()->get_new_page_url( $this->order->get_type() );
|
||||
|
||||
?>
|
||||
<div class="wrap">
|
||||
@@ -298,9 +375,20 @@ class Edit {
|
||||
?>
|
||||
>
|
||||
<?php wp_nonce_field( $this->get_order_edit_nonce_action() ); ?>
|
||||
<?php
|
||||
/**
|
||||
* Fires at the top of the order edit form. Can be used as a replacement for edit_form_top hook for HPOS.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*/
|
||||
do_action( 'order_edit_form_top', $this->order );
|
||||
?>
|
||||
<input type="hidden" id="hiddenaction" name="action" value="<?php echo esc_attr( $form_action ); ?>"/>
|
||||
<input type="hidden" id="original_order_status" name="original_order_status" value="<?php echo esc_attr( $this->order->get_status() ); ?>"/>
|
||||
<input type="hidden" id="referredby" name="referredby" value="<?php echo $referer ? esc_url( $referer ) : ''; ?>"/>
|
||||
<input type="hidden" id="post_ID" name="post_ID" value="<?php echo esc_attr( $this->order->get_id() ); ?>"/>
|
||||
<div id="poststuff">
|
||||
<div id="post-body"
|
||||
class="metabox-holder columns-<?php echo ( 1 === get_current_screen()->get_columns() ) ? '1' : '2'; ?>">
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders;
|
||||
|
||||
/**
|
||||
* This class takes care of the edit lock logic when HPOS is enabled.
|
||||
* For better interoperability with WordPress, edit locks are stored in the same format as posts. That is, as a metadata
|
||||
* in the order object (key: '_edit_lock') in the format "timestamp:user_id".
|
||||
*
|
||||
* @since 7.8.0
|
||||
*/
|
||||
class EditLock {
|
||||
|
||||
const META_KEY_NAME = '_edit_lock';
|
||||
|
||||
/**
|
||||
* Obtains lock information for a given order. If the lock has expired or it's assigned to an invalid user,
|
||||
* the order is no longer considered locked.
|
||||
*
|
||||
* @param \WC_Order $order Order to check.
|
||||
* @return bool|array
|
||||
*/
|
||||
public function get_lock( \WC_Order $order ) {
|
||||
$lock = $order->get_meta( self::META_KEY_NAME, true, 'edit' );
|
||||
if ( ! $lock ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lock = explode( ':', $lock );
|
||||
if ( 2 !== count( $lock ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$time = absint( $lock[0] );
|
||||
$user_id = isset( $lock[1] ) ? absint( $lock[1] ) : 0;
|
||||
|
||||
if ( ! $time || ! get_user_by( 'id', $user_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** This filter is documented in WP's wp-admin/includes/ajax-actions.php */
|
||||
$time_window = apply_filters( 'wp_check_post_lock_window', 150 ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
if ( time() >= ( $time + $time_window ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return compact( 'time', 'user_id' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the order is being edited (i.e. locked) by another user.
|
||||
*
|
||||
* @param \WC_Order $order Order to check.
|
||||
* @return bool TRUE if order is locked and currently being edited by another user. FALSE otherwise.
|
||||
*/
|
||||
public function is_locked_by_another_user( \WC_Order $order ) : bool {
|
||||
$lock = $this->get_lock( $order );
|
||||
return $lock && ( get_current_user_id() !== $lock['user_id'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the order is being edited by any user.
|
||||
*
|
||||
* @param \WC_Order $order Order to check.
|
||||
* @return boolean TRUE if order is locked and currently being edited by a user. FALSE otherwise.
|
||||
*/
|
||||
public function is_locked( \WC_Order $order ) : bool {
|
||||
return (bool) $this->get_lock( $order );
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns an order's edit lock to the current user.
|
||||
*
|
||||
* @param \WC_Order $order The order to apply the lock to.
|
||||
* @return array|bool FALSE if no user is logged-in, an array in the same format as {@see get_lock()} otherwise.
|
||||
*/
|
||||
public function lock( \WC_Order $order ) {
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
if ( ! $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$order->update_meta_data( self::META_KEY_NAME, time() . ':' . $user_id );
|
||||
$order->save_meta_data();
|
||||
|
||||
return $order->get_meta( self::META_KEY_NAME, true, 'edit' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked to 'heartbeat_received' on the edit order page to refresh the lock on an order being edited by the current user.
|
||||
*
|
||||
* @param array $response The heartbeat response to be sent.
|
||||
* @param array $data Data sent through the heartbeat.
|
||||
* @return array Response to be sent.
|
||||
*/
|
||||
public function refresh_lock_ajax( $response, $data ) {
|
||||
$order_id = absint( $data['wc-refresh-order-lock'] ?? 0 );
|
||||
if ( ! $order_id ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( ! $order || ( ! current_user_can( get_post_type_object( $order->get_type() )->cap->edit_post, $order->get_id() ) && ! current_user_can( 'manage_woocommerce' ) ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response['wc-refresh-order-lock'] = array();
|
||||
|
||||
if ( ! $this->is_locked_by_another_user( $order ) ) {
|
||||
$response['wc-refresh-order-lock']['lock'] = $this->lock( $order );
|
||||
} else {
|
||||
$current_lock = $this->get_lock( $order );
|
||||
$user = get_user_by( 'id', $current_lock['user_id'] );
|
||||
|
||||
$response['wc-refresh-order-lock']['error'] = array(
|
||||
// translators: %s is a user's name.
|
||||
'message' => sprintf( __( '%s has taken over and is currently editing.', 'woocommerce' ), $user->display_name ),
|
||||
'user_name' => $user->display_name,
|
||||
'user_avatar_src' => get_option( 'show_avatars' ) ? get_avatar_url( $user->ID, array( 'size' => 64 ) ) : '',
|
||||
'user_avatar_src_2x' => get_option( 'show_avatars' ) ? get_avatar_url( $user->ID, array( 'size' => 128 ) ) : '',
|
||||
);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked to 'heartbeat_received' on the orders screen to refresh the locked status of orders in the list table.
|
||||
*
|
||||
* @param array $response The heartbeat response to be sent.
|
||||
* @param array $data Data sent through the heartbeat.
|
||||
* @return array Response to be sent.
|
||||
*/
|
||||
public function check_locked_orders_ajax( $response, $data ) {
|
||||
if ( empty( $data['wc-check-locked-orders'] ) || ! is_array( $data['wc-check-locked-orders'] ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response['wc-check-locked-orders'] = array();
|
||||
|
||||
$order_ids = array_unique( array_map( 'absint', $data['wc-check-locked-orders'] ) );
|
||||
foreach ( $order_ids as $order_id ) {
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( ! $order ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! $this->is_locked_by_another_user( $order ) || ( ! current_user_can( get_post_type_object( $order->get_type() )->cap->edit_post, $order->get_id() ) && ! current_user_can( 'manage_woocommerce' ) ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$response['wc-check-locked-orders'][ $order_id ] = true;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs HTML for the lock dialog based on the status of the lock on the order (if any).
|
||||
* Depending on who owns the lock, this could be a message with the chance to take over or a message indicating that
|
||||
* someone else has taken over the order.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_dialog( $order ) {
|
||||
$locked = $this->is_locked_by_another_user( $order );
|
||||
$lock = $this->get_lock( $order );
|
||||
$user = get_user_by( 'id', $lock['user_id'] );
|
||||
|
||||
$edit_url = wc_get_container()->get( \Automattic\WooCommerce\Internal\Admin\Orders\PageController::class )->get_edit_url( $order->get_id() );
|
||||
|
||||
$sendback_url = wp_get_referer();
|
||||
if ( ! $sendback_url ) {
|
||||
$sendback_url = wc_get_container()->get( \Automattic\WooCommerce\Internal\Admin\Orders\PageController::class )->get_base_page_url( $order->get_type() );
|
||||
}
|
||||
|
||||
$sendback_text = __( 'Go back', 'woocommerce' );
|
||||
?>
|
||||
<div id="post-lock-dialog" class="notification-dialog-wrap <?php echo $locked ? '' : 'hidden'; ?> order-lock-dialog">
|
||||
<div class="notification-dialog-background"></div>
|
||||
<div class="notification-dialog">
|
||||
<?php if ( $locked ) : ?>
|
||||
<div class="post-locked-message">
|
||||
<div class="post-locked-avatar"><?php echo get_avatar( $user->ID, 64 ); ?></div>
|
||||
<p class="currently-editing wp-tab-first" tabindex="0">
|
||||
<?php
|
||||
// translators: %s is a user's name.
|
||||
echo esc_html( sprintf( __( '%s is currently editing this order. Do you want to take over?', 'woocommerce' ), esc_html( $user->display_name ) ) );
|
||||
?>
|
||||
</p>
|
||||
<p>
|
||||
<a class="button" href="<?php echo esc_url( $sendback_url ); ?>"><?php echo esc_html( $sendback_text ); ?></a>
|
||||
<a class="button button-primary wp-tab-last" href="<?php echo esc_url( add_query_arg( 'claim-lock', '1', wp_nonce_url( $edit_url, 'claim-lock-' . $order->get_id() ) ) ); ?>"><?php esc_html_e( 'Take over', 'woocommerce' ); ?></a>
|
||||
</p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="post-taken-over">
|
||||
<div class="post-locked-avatar"></div>
|
||||
<p class="wp-tab-first" tabindex="0">
|
||||
<span class="currently-editing"></span><br />
|
||||
</p>
|
||||
<p><a class="button button-primary wp-tab-last" href="<?php echo esc_url( $sendback_url ); ?>"><?php echo esc_html( $sendback_text ); ?></a></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace Automattic\WooCommerce\Internal\Admin\Orders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
|
||||
use Automattic\WooCommerce\Utilities\OrderUtil;
|
||||
use WC_Order;
|
||||
use WP_List_Table;
|
||||
use WP_Screen;
|
||||
@@ -109,6 +110,44 @@ class ListTable extends WP_List_Table {
|
||||
add_action( 'manage_' . wc_get_page_screen_id( $this->order_type ) . '_custom_column', array( $this, 'render_column' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates content for a single row of the table.
|
||||
*
|
||||
* @since 7.8.0
|
||||
*
|
||||
* @param \WC_Order $order The current order.
|
||||
*/
|
||||
public function single_row( $order ) {
|
||||
/**
|
||||
* Filters the list of CSS class names for a given order row in the orders list table.
|
||||
*
|
||||
* @since 7.8.0
|
||||
*
|
||||
* @param string[] $classes An array of CSS class names.
|
||||
* @param \WC_Order $order The order object.
|
||||
*/
|
||||
$css_classes = apply_filters(
|
||||
'woocommerce_' . $this->order_type . '_list_table_order_css_classes',
|
||||
array(
|
||||
'order-' . $order->get_id(),
|
||||
'type-' . $order->get_type(),
|
||||
'status-' . $order->get_status(),
|
||||
),
|
||||
$order
|
||||
);
|
||||
$css_classes = array_unique( array_map( 'trim', $css_classes ) );
|
||||
|
||||
// Is locked?
|
||||
$edit_lock = wc_get_container()->get( EditLock::class );
|
||||
if ( $edit_lock->is_locked_by_another_user( $order ) ) {
|
||||
$css_classes[] = 'wp-locked';
|
||||
}
|
||||
|
||||
echo '<tr id="order-' . esc_attr( $order->get_id() ) . '" class="' . esc_attr( implode( ' ', $css_classes ) ) . '">';
|
||||
$this->single_row_columns( $order );
|
||||
echo '</tr>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render individual column.
|
||||
*
|
||||
@@ -276,6 +315,37 @@ class ListTable extends WP_List_Table {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of CSS classes for the WP_List_Table table tag.
|
||||
*
|
||||
* @since 7.8.0
|
||||
*
|
||||
* @return string[] Array of CSS classes for the table tag.
|
||||
*/
|
||||
protected function get_table_classes() {
|
||||
/**
|
||||
* Filters the list of CSS class names for the orders list table.
|
||||
*
|
||||
* @since 7.8.0
|
||||
*
|
||||
* @param string[] $classes An array of CSS class names.
|
||||
* @param string $order_type The order type.
|
||||
*/
|
||||
$css_classes = apply_filters(
|
||||
'woocommerce_' . $this->order_type . '_list_table_css_classes',
|
||||
array_merge(
|
||||
parent::get_table_classes(),
|
||||
array(
|
||||
'wc-orders-list-table',
|
||||
'wc-orders-list-table-' . $this->order_type,
|
||||
)
|
||||
),
|
||||
$this->order_type
|
||||
);
|
||||
|
||||
return array_unique( array_map( 'trim', $css_classes ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the list of items for displaying.
|
||||
*/
|
||||
@@ -471,7 +541,7 @@ class ListTable extends WP_List_Table {
|
||||
$view_counts[ $slug ] = $total_in_status;
|
||||
}
|
||||
|
||||
if ( ( get_post_status_object( $slug ) )->show_in_admin_all_list ) {
|
||||
if ( ( get_post_status_object( $slug ) )->show_in_admin_all_list && 'auto-draft' !== $slug ) {
|
||||
$all_count += $total_in_status;
|
||||
}
|
||||
}
|
||||
@@ -550,8 +620,9 @@ class ListTable extends WP_List_Table {
|
||||
array_merge(
|
||||
wc_get_order_statuses(),
|
||||
array(
|
||||
'trash' => ( get_post_status_object( 'trash' ) )->label,
|
||||
'draft' => ( get_post_status_object( 'draft' ) )->label,
|
||||
'trash' => ( get_post_status_object( 'trash' ) )->label,
|
||||
'draft' => ( get_post_status_object( 'draft' ) )->label,
|
||||
'auto-draft' => ( get_post_status_object( 'auto-draft' ) )->label,
|
||||
)
|
||||
),
|
||||
array_flip( get_post_stati( array( 'show_in_admin_status_list' => true ) ) )
|
||||
@@ -794,7 +865,21 @@ class ListTable extends WP_List_Table {
|
||||
* @return string
|
||||
*/
|
||||
public function column_cb( $item ) {
|
||||
return sprintf( '<input type="checkbox" name="%1$s[]" value="%2$s" />', esc_attr( $this->_args['singular'] ), esc_attr( $item->get_id() ) );
|
||||
ob_start();
|
||||
?>
|
||||
<input id="cb-select-<?php echo esc_attr( $item->get_id() ); ?>" type="checkbox" name="id[]" value="<?php echo esc_attr( $item->get_id() ); ?>" />
|
||||
|
||||
<div class="locked-indicator">
|
||||
<span class="locked-indicator-icon" aria-hidden="true"></span>
|
||||
<span class="screen-reader-text">
|
||||
<?php
|
||||
// translators: %s is an order ID.
|
||||
echo esc_html( sprintf( __( 'Order %s is locked.', 'woocommerce' ), $item->get_id() ) );
|
||||
?>
|
||||
</span>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -916,7 +1001,7 @@ class ListTable extends WP_List_Table {
|
||||
}
|
||||
|
||||
// Gracefully handle legacy statuses.
|
||||
if ( in_array( $order->get_status(), array( 'trash', 'draft' ), true ) ) {
|
||||
if ( in_array( $order->get_status(), array( 'trash', 'draft', 'auto-draft' ), true ) ) {
|
||||
$status_name = ( get_post_status_object( $order->get_status() ) )->label;
|
||||
} else {
|
||||
$status_name = wc_get_order_status_name( $order->get_status() );
|
||||
@@ -1112,7 +1197,7 @@ class ListTable extends WP_List_Table {
|
||||
|
||||
$action = 'delete';
|
||||
} else {
|
||||
$ids = isset( $_REQUEST['order'] ) ? array_reverse( array_map( 'absint', $_REQUEST['order'] ) ) : array();
|
||||
$ids = isset( $_REQUEST['id'] ) ? array_reverse( array_map( 'absint', (array) $_REQUEST['id'] ) ) : array();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1132,8 +1217,9 @@ class ListTable extends WP_List_Table {
|
||||
exit;
|
||||
}
|
||||
|
||||
$report_action = '';
|
||||
$changed = 0;
|
||||
$report_action = '';
|
||||
$changed = 0;
|
||||
$action_handled = true;
|
||||
|
||||
if ( 'remove_personal_data' === $action ) {
|
||||
$report_action = 'removed_personal_data';
|
||||
@@ -1154,8 +1240,15 @@ class ListTable extends WP_List_Table {
|
||||
|
||||
if ( isset( $order_statuses[ 'wc-' . $new_status ] ) ) {
|
||||
$changed = $this->do_bulk_action_mark_orders( $ids, $new_status );
|
||||
} else {
|
||||
$action_handled = false;
|
||||
}
|
||||
} else {
|
||||
$action_handled = false;
|
||||
}
|
||||
|
||||
// Custom action.
|
||||
if ( ! $action_handled ) {
|
||||
$screen = get_current_screen()->id;
|
||||
|
||||
/**
|
||||
@@ -1247,13 +1340,11 @@ class ListTable extends WP_List_Table {
|
||||
* @return int Number of orders that were trashed.
|
||||
*/
|
||||
private function do_delete( array $ids, bool $force_delete = false ): int {
|
||||
$orders_store = wc_get_container()->get( OrdersTableDataStore::class );
|
||||
$delete_args = $force_delete ? array( 'force_delete' => true ) : array();
|
||||
$changed = 0;
|
||||
|
||||
foreach ( $ids as $id ) {
|
||||
$order = wc_get_order( $id );
|
||||
$orders_store->delete( $order, $delete_args );
|
||||
$order->delete( $force_delete );
|
||||
$updated_order = wc_get_order( $id );
|
||||
|
||||
if ( ( $force_delete && false === $updated_order ) || ( ! $force_delete && $updated_order->get_status() === 'trash' ) ) {
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
|
||||
|
||||
/**
|
||||
* TaxonomiesMetaBox class, renders taxonomy sidebar widget on order edit screen.
|
||||
*/
|
||||
class TaxonomiesMetaBox {
|
||||
|
||||
/**
|
||||
* Order Table data store class.
|
||||
*
|
||||
* @var OrdersTableDataStore
|
||||
*/
|
||||
private $orders_table_data_store;
|
||||
|
||||
/**
|
||||
* Dependency injection init method.
|
||||
*
|
||||
* @param OrdersTableDataStore $orders_table_data_store Order Table data store class.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init( OrdersTableDataStore $orders_table_data_store ) {
|
||||
$this->orders_table_data_store = $orders_table_data_store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers meta boxes to be rendered in order edit screen for taxonomies.
|
||||
*
|
||||
* Note: This is re-implementation of part of WP core's `register_and_do_post_meta_boxes` function. Since the code block that add meta box for taxonomies is not filterable, we have to re-implement it.
|
||||
*
|
||||
* @param string $screen_id Screen ID.
|
||||
* @param string $order_type Order type to register meta boxes for.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_taxonomies_meta_boxes( string $screen_id, string $order_type ) {
|
||||
include_once ABSPATH . 'wp-admin/includes/meta-boxes.php';
|
||||
$taxonomies = get_object_taxonomies( $order_type );
|
||||
// All taxonomies.
|
||||
foreach ( $taxonomies as $tax_name ) {
|
||||
$taxonomy = get_taxonomy( $tax_name );
|
||||
if ( ! $taxonomy->show_ui || false === $taxonomy->meta_box_cb ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 'post_categories_meta_box' === $taxonomy->meta_box_cb ) {
|
||||
$taxonomy->meta_box_cb = array( $this, 'order_categories_meta_box' );
|
||||
}
|
||||
|
||||
if ( 'post_tags_meta_box' === $taxonomy->meta_box_cb ) {
|
||||
$taxonomy->meta_box_cb = array( $this, 'order_tags_meta_box' );
|
||||
}
|
||||
|
||||
$label = $taxonomy->labels->name;
|
||||
|
||||
if ( ! is_taxonomy_hierarchical( $tax_name ) ) {
|
||||
$tax_meta_box_id = 'tagsdiv-' . $tax_name;
|
||||
} else {
|
||||
$tax_meta_box_id = $tax_name . 'div';
|
||||
}
|
||||
|
||||
add_meta_box(
|
||||
$tax_meta_box_id,
|
||||
$label,
|
||||
$taxonomy->meta_box_cb,
|
||||
$screen_id,
|
||||
'side',
|
||||
'core',
|
||||
array(
|
||||
'taxonomy' => $tax_name,
|
||||
'__back_compat_meta_box' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save handler for taxonomy data.
|
||||
*
|
||||
* @param \WC_Abstract_Order $order Order object.
|
||||
* @param array|null $taxonomy_input Taxonomy input passed from input.
|
||||
*/
|
||||
public function save_taxonomies( \WC_Abstract_Order $order, $taxonomy_input ) {
|
||||
if ( ! isset( $taxonomy_input ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sanitized_tax_input = $this->sanitize_tax_input( $taxonomy_input );
|
||||
|
||||
$sanitized_tax_input = $this->orders_table_data_store->init_default_taxonomies( $order, $sanitized_tax_input );
|
||||
$this->orders_table_data_store->set_custom_taxonomies( $order, $sanitized_tax_input );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize taxonomy input by calling sanitize callbacks for each registered taxonomy.
|
||||
*
|
||||
* @param array|null $taxonomy_data Nonce verified taxonomy input.
|
||||
*
|
||||
* @return array Sanitized taxonomy input.
|
||||
*/
|
||||
private function sanitize_tax_input( $taxonomy_data ) : array {
|
||||
$sanitized_tax_input = array();
|
||||
if ( ! is_array( $taxonomy_data ) ) {
|
||||
return $sanitized_tax_input;
|
||||
}
|
||||
|
||||
// Convert taxonomy input to term IDs, to avoid ambiguity.
|
||||
foreach ( $taxonomy_data as $taxonomy => $terms ) {
|
||||
$tax_object = get_taxonomy( $taxonomy );
|
||||
if ( $tax_object && isset( $tax_object->meta_box_sanitize_cb ) ) {
|
||||
$sanitized_tax_input[ $taxonomy ] = call_user_func_array( $tax_object->meta_box_sanitize_cb, array( $taxonomy, $terms ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $sanitized_tax_input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the categories meta box to the order screen. This is just a wrapper around the post_categories_meta_box.
|
||||
*
|
||||
* @param \WC_Abstract_Order $order Order object.
|
||||
* @param array $box Meta box args.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function order_categories_meta_box( $order, $box ) {
|
||||
$post = get_post( $order->get_id() );
|
||||
post_categories_meta_box( $post, $box );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the tags meta box to the order screen. This is just a wrapper around the post_tags_meta_box.
|
||||
*
|
||||
* @param \WC_Abstract_Order $order Order object.
|
||||
* @param array $box Meta box args.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function order_tags_meta_box( $order, $box ) {
|
||||
$post = get_post( $order->get_id() );
|
||||
post_tags_meta_box( $post, $box );
|
||||
}
|
||||
}
|
||||
@@ -91,12 +91,48 @@ class PageController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Claims the lock for the order being edited/created (unless it belongs to someone else).
|
||||
* Also handles the 'claim-lock' action which allows taking over the order forcefully.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_edit_lock() {
|
||||
if ( ! $this->order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$edit_lock = wc_get_container()->get( EditLock::class );
|
||||
|
||||
$locked = $edit_lock->is_locked_by_another_user( $this->order );
|
||||
|
||||
// Take over order?
|
||||
if ( ! empty( $_GET['claim-lock'] ) && wp_verify_nonce( $_GET['_wpnonce'] ?? '', 'claim-lock-' . $this->order->get_id() ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$edit_lock->lock( $this->order );
|
||||
wp_safe_redirect( $this->get_edit_url( $this->order->get_id() ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( ! $locked ) {
|
||||
$edit_lock->lock( $this->order );
|
||||
}
|
||||
|
||||
add_action(
|
||||
'admin_footer',
|
||||
function() use ( $edit_lock ) {
|
||||
$edit_lock->render_dialog( $this->order );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the page controller, including registering the menu item.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setup(): void {
|
||||
global $plugin_page, $pagenow;
|
||||
|
||||
$this->redirection_controller = new PostsRedirectionController( $this );
|
||||
|
||||
// Register menu.
|
||||
@@ -106,34 +142,81 @@ class PageController {
|
||||
add_action( 'admin_menu', 'register_menu', 9 );
|
||||
}
|
||||
|
||||
// Not on an Orders page.
|
||||
if ( 'admin.php' !== $pagenow || 0 !== strpos( $plugin_page, 'wc-orders' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->set_order_type();
|
||||
$this->set_action();
|
||||
|
||||
$page_suffix = ( 'shop_order' === $this->order_type ? '' : '--' . $this->order_type );
|
||||
|
||||
self::add_action( 'load-woocommerce_page_wc-orders' . $page_suffix, array( $this, 'handle_load_page_action' ) );
|
||||
self::add_action( 'admin_title', array( $this, 'set_page_title' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform initialization for the current action.
|
||||
*/
|
||||
private function handle_load_page_action() {
|
||||
$screen = get_current_screen();
|
||||
$screen->post_type = $this->order_type;
|
||||
|
||||
if ( method_exists( $this, 'setup_action_' . $this->current_action ) ) {
|
||||
$this->{"setup_action_{$this->current_action}"}();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the document title for Orders screens to match what it would be with the shop_order CPT.
|
||||
*
|
||||
* @param string $admin_title The admin screen title before it's filtered.
|
||||
*
|
||||
* @return string The filtered admin title.
|
||||
*/
|
||||
private function set_page_title( $admin_title ) {
|
||||
if ( ! $this->is_order_screen( $this->order_type ) ) {
|
||||
return $admin_title;
|
||||
}
|
||||
|
||||
$wp_order_type = get_post_type_object( $this->order_type );
|
||||
$labels = get_post_type_labels( $wp_order_type );
|
||||
|
||||
if ( $this->is_order_screen( $this->order_type, 'list' ) ) {
|
||||
$admin_title = sprintf(
|
||||
// translators: 1: The label for an order type 2: The name of the website.
|
||||
esc_html__( '%1$s ‹ %2$s — WordPress', 'woocommerce' ),
|
||||
esc_html( $labels->name ),
|
||||
esc_html( get_bloginfo( 'name' ) )
|
||||
);
|
||||
} elseif ( $this->is_order_screen( $this->order_type, 'edit' ) ) {
|
||||
$admin_title = sprintf(
|
||||
// translators: 1: The label for an order type 2: The title of the order 3: The name of the website.
|
||||
esc_html__( '%1$s #%2$s ‹ %3$s — WordPress', 'woocommerce' ),
|
||||
esc_html( $labels->edit_item ),
|
||||
absint( $this->order->get_id() ),
|
||||
esc_html( get_bloginfo( 'name' ) )
|
||||
);
|
||||
} elseif ( $this->is_order_screen( $this->order_type, 'new' ) ) {
|
||||
$admin_title = sprintf(
|
||||
// translators: 1: The label for an order type 2: The name of the website.
|
||||
esc_html__( '%1$s ‹ %2$s — WordPress', 'woocommerce' ),
|
||||
esc_html( $labels->add_new_item ),
|
||||
esc_html( get_bloginfo( 'name' ) )
|
||||
);
|
||||
}
|
||||
|
||||
return $admin_title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the order type for the current screen.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function set_order_type() {
|
||||
global $plugin_page, $pagenow;
|
||||
|
||||
if ( 'admin.php' !== $pagenow || 0 !== strpos( $plugin_page, 'wc-orders' ) ) {
|
||||
return;
|
||||
}
|
||||
global $plugin_page;
|
||||
|
||||
$this->order_type = str_replace( array( 'wc-orders--', 'wc-orders' ), '', $plugin_page );
|
||||
$this->order_type = empty( $this->order_type ) ? 'shop_order' : $this->order_type;
|
||||
@@ -207,11 +290,6 @@ class PageController {
|
||||
switch ( $this->current_action ) {
|
||||
case 'edit_order':
|
||||
case 'new_order':
|
||||
if ( ! isset( $this->order_edit_form ) ) {
|
||||
$this->order_edit_form = new Edit();
|
||||
$this->order_edit_form->setup( $this->order );
|
||||
}
|
||||
$this->order_edit_form->set_current_action( $this->current_action );
|
||||
$this->order_edit_form->display();
|
||||
break;
|
||||
case 'list_orders':
|
||||
@@ -257,6 +335,22 @@ class PageController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the order edit form for creating or editing an order.
|
||||
*
|
||||
* @see \Automattic\WooCommerce\Internal\Admin\Orders\Edit.
|
||||
* @since 8.1.0
|
||||
*/
|
||||
private function prepare_order_edit_form(): void {
|
||||
if ( ! $this->order || ! in_array( $this->current_action, array( 'new_order', 'edit_order' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->order_edit_form = $this->order_edit_form ?? new Edit();
|
||||
$this->order_edit_form->setup( $this->order );
|
||||
$this->order_edit_form->set_current_action( $this->current_action );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles initialization of the orders edit form.
|
||||
*
|
||||
@@ -266,7 +360,10 @@ class PageController {
|
||||
global $theorder;
|
||||
$this->order = wc_get_order( absint( isset( $_GET['id'] ) ? $_GET['id'] : 0 ) );
|
||||
$this->verify_edit_permission();
|
||||
$this->handle_edit_lock();
|
||||
$theorder = $this->order;
|
||||
|
||||
$this->prepare_order_edit_form();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,10 +383,18 @@ class PageController {
|
||||
|
||||
$this->order = new $order_class_name();
|
||||
$this->order->set_object_read( false );
|
||||
$this->order->set_status( 'pending' );
|
||||
$this->order->set_status( 'auto-draft' );
|
||||
$this->order->save();
|
||||
$this->handle_edit_lock();
|
||||
|
||||
// Schedule auto-draft cleanup. We re-use the WP event here on purpose.
|
||||
if ( ! wp_next_scheduled( 'wp_scheduled_auto_draft_delete' ) ) {
|
||||
wp_schedule_event( time(), 'daily', 'wp_scheduled_auto_draft_delete' );
|
||||
}
|
||||
|
||||
$theorder = $this->order;
|
||||
|
||||
$this->prepare_order_edit_form();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,4 +489,89 @@ class PageController {
|
||||
return admin_url( 'admin.php?page=wc-orders' . ( 'shop_order' === $order_type ? '' : '--' . $order_type ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check if the current admin screen is related to orders.
|
||||
*
|
||||
* @param string $type Optional. The order type to check for. Default shop_order.
|
||||
* @param string $action Optional. The purpose of the screen to check for. 'list', 'edit', or 'new'.
|
||||
* Leave empty to check for any order screen.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_order_screen( $type = 'shop_order', $action = '' ) : bool {
|
||||
if ( ! did_action( 'current_screen' ) ) {
|
||||
wc_doing_it_wrong(
|
||||
__METHOD__,
|
||||
sprintf(
|
||||
// translators: %s is the name of a function.
|
||||
esc_html__( '%s must be called after the current_screen action.', 'woocommerce' ),
|
||||
esc_html( __METHOD__ )
|
||||
),
|
||||
'7.9.0'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$valid_types = wc_get_order_types( 'view-order' );
|
||||
if ( ! in_array( $type, $valid_types, true ) ) {
|
||||
wc_doing_it_wrong(
|
||||
__METHOD__,
|
||||
sprintf(
|
||||
// translators: %s is the name of an order type.
|
||||
esc_html__( '%s is not a valid order type.', 'woocommerce' ),
|
||||
esc_html( $type )
|
||||
),
|
||||
'7.9.0'
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) {
|
||||
if ( $action ) {
|
||||
switch ( $action ) {
|
||||
case 'edit':
|
||||
$is_action = 'edit_order' === $this->current_action;
|
||||
break;
|
||||
case 'list':
|
||||
$is_action = 'list_orders' === $this->current_action;
|
||||
break;
|
||||
case 'new':
|
||||
$is_action = 'new_order' === $this->current_action;
|
||||
break;
|
||||
default:
|
||||
$is_action = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$type_match = $type === $this->order_type;
|
||||
$action_match = ! $action || $is_action;
|
||||
} else {
|
||||
$screen = get_current_screen();
|
||||
|
||||
if ( $action ) {
|
||||
switch ( $action ) {
|
||||
case 'edit':
|
||||
$screen_match = 'post' === $screen->base && filter_input( INPUT_GET, 'post', FILTER_VALIDATE_INT );
|
||||
break;
|
||||
case 'list':
|
||||
$screen_match = 'edit' === $screen->base;
|
||||
break;
|
||||
case 'new':
|
||||
$screen_match = 'post' === $screen->base && 'add' === $screen->action;
|
||||
break;
|
||||
default:
|
||||
$screen_match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$type_match = $type === $screen->post_type;
|
||||
$action_match = ! $action || $screen_match;
|
||||
}
|
||||
|
||||
return $type_match && $action_match;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ class PostsRedirectionController {
|
||||
$new_url = add_query_arg(
|
||||
array(
|
||||
'action' => $action,
|
||||
'order' => $posts,
|
||||
'id' => $posts,
|
||||
'_wp_http_referer' => $this->page_controller->get_orders_url(),
|
||||
'_wpnonce' => wp_create_nonce( 'bulk-orders' ),
|
||||
),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,8 @@ use Automattic\WooCommerce\Admin\API\Plugins;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\Admin\API\Reports\Orders\DataStore as OrdersDataStore;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Internal\Admin\WCPayPromotion\Init as WCPayPromotionInit;
|
||||
use Automattic\WooCommerce\Utilities\FeaturesUtil;
|
||||
use WC_Marketplace_Suggestions;
|
||||
|
||||
/**
|
||||
@@ -105,7 +107,7 @@ class Settings {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks extra neccessary data into the component settings array already set in WooCommerce core.
|
||||
* Hooks extra necessary data into the component settings array already set in WooCommerce core.
|
||||
*
|
||||
* @param array $settings Array of component settings.
|
||||
* @return array Array of component settings.
|
||||
@@ -233,9 +235,32 @@ class Settings {
|
||||
$settings['connectNonce'] = wp_create_nonce( 'connect' );
|
||||
$settings['wcpay_welcome_page_connect_nonce'] = wp_create_nonce( 'wcpay-connect' );
|
||||
|
||||
$settings['features'] = $this->get_features();
|
||||
|
||||
$settings['isWooPayEligible'] = WCPayPromotionInit::is_woopay_eligible();
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes non necesary feature properties for the client side.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_features() {
|
||||
$features = FeaturesUtil::get_features( true, true );
|
||||
$new_features = array();
|
||||
|
||||
foreach ( array_keys( $features ) as $feature_id ) {
|
||||
$new_features[ $feature_id ] = array(
|
||||
'is_enabled' => $features[ $feature_id ]['is_enabled'],
|
||||
'is_experimental' => $features[ $feature_id ]['is_experimental'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
return $new_features;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the admin settings for use in the WC REST API
|
||||
*
|
||||
|
||||
@@ -22,6 +22,14 @@ class WCAdminAssets {
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* An array of dependencies that have been preloaded (to avoid duplicates).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $preloaded_dependencies;
|
||||
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
@@ -238,7 +246,7 @@ class WCAdminAssets {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all the neccessary scripts and styles to show the admin experience.
|
||||
* Registers all the necessary scripts and styles to show the admin experience.
|
||||
*/
|
||||
public function register_scripts() {
|
||||
if ( ! function_exists( 'wp_set_script_translations' ) ) {
|
||||
@@ -278,6 +286,7 @@ class WCAdminAssets {
|
||||
'wc-date',
|
||||
'wc-components',
|
||||
'wc-customer-effort-score',
|
||||
'wc-experimental',
|
||||
WC_ADMIN_APP,
|
||||
);
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ class WCAdminUser {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to retrive user data fields.
|
||||
* Helper to retrieve user data fields.
|
||||
*
|
||||
* Migrates old key prefixes as well.
|
||||
*
|
||||
|
||||
@@ -141,6 +141,15 @@ class Init {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get merchant WooPay eligibility.
|
||||
*/
|
||||
public static function is_woopay_eligible() {
|
||||
$wcpay_promotion = self::get_wc_pay_promotion_spec();
|
||||
|
||||
return $wcpay_promotion && 'woocommerce_payments:woopay' === $wcpay_promotion->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the specs transient.
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\WooCommercePayments;
|
||||
use Automattic\WooCommerce\Admin\WCAdminHelper;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
|
||||
/**
|
||||
* Class WCPayWelcomePage
|
||||
@@ -11,121 +12,106 @@ use Automattic\WooCommerce\Admin\WCAdminHelper;
|
||||
* @package Automattic\WooCommerce\Admin\Features
|
||||
*/
|
||||
class WcPayWelcomePage {
|
||||
const CACHE_TRANSIENT_NAME = 'wcpay_welcome_page_incentive';
|
||||
const HAD_WCPAY_OPTION_NAME = 'wcpay_was_in_use';
|
||||
|
||||
const EXPERIMENT_NAME = 'woocommerce_payments_menu_promo_us_2022';
|
||||
const OTHER_GATEWAYS = [
|
||||
'affirm',
|
||||
'afterpay',
|
||||
'amazon_payments_advanced_express',
|
||||
'amazon_payments_advanced',
|
||||
'authorize_net_cim_credit_card',
|
||||
'authorize_net_cim_echeck',
|
||||
'bacs',
|
||||
'bambora_credit_card',
|
||||
'braintree_credit_card',
|
||||
'braintree_paypal',
|
||||
'chase_paymentech',
|
||||
'cybersource_credit_card',
|
||||
'elavon_converge_credit_card',
|
||||
'elavon_converge_echeck',
|
||||
'gocardless',
|
||||
'intuit_payments_credit_card',
|
||||
'intuit_payments_echeck',
|
||||
'kco',
|
||||
'klarna_payments',
|
||||
'payfast',
|
||||
'paypal',
|
||||
'paytrace',
|
||||
'ppcp-gateway',
|
||||
'psigate',
|
||||
'sagepaymentsusaapi',
|
||||
'square_credit_card',
|
||||
'stripe_alipay',
|
||||
'stripe_multibanco',
|
||||
'stripe',
|
||||
'trustcommerce',
|
||||
'usa_epay_credit_card',
|
||||
];
|
||||
/**
|
||||
* Plugin instance.
|
||||
*
|
||||
* @var WcPayWelcomePage
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Main Instance.
|
||||
*/
|
||||
public static function instance() {
|
||||
self::$instance = is_null( self::$instance ) ? new self() : self::$instance;
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eligible incentive for the store.
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
private $incentive = null;
|
||||
|
||||
/**
|
||||
* WCPayWelcomePage constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'admin_menu', array( $this, 'register_payments_welcome_page' ) );
|
||||
add_action( 'admin_menu', [ $this, 'register_payments_welcome_page' ] );
|
||||
add_filter( 'woocommerce_admin_shared_settings', [ $this, 'shared_settings' ] );
|
||||
add_filter( 'woocommerce_admin_allowed_promo_notes', [ $this, 'allowed_promo_notes' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the WooCommerce Payments welcome page.
|
||||
* Whether the WooPayments welcome page should be visible.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function register_payments_welcome_page() {
|
||||
global $menu;
|
||||
|
||||
// WC Payment must not be installed.
|
||||
if ( WooCommercePayments::is_installed() ) {
|
||||
return;
|
||||
public function must_be_visible(): bool {
|
||||
// The WooPayments plugin must not be active.
|
||||
if ( $this->is_wcpay_active() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Live store for at least 90 days.
|
||||
if ( ! WCAdminHelper::is_wc_admin_active_for( DAY_IN_SECONDS * 90 ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Must be a US based business.
|
||||
if ( WC()->countries->get_base_country() !== 'US' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Has another payment gateway installed.
|
||||
if ( ! $this->is_another_payment_gateway_installed() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No existing WCPay account.
|
||||
if ( $this->has_wcpay_account() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Suggestions may be disabled via a setting.
|
||||
// Suggestions not disabled via a setting.
|
||||
if ( get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) === 'no' ) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter allow marketplace suggestions.
|
||||
*
|
||||
* User can disabled all suggestions via filter.
|
||||
* User can disable all suggestions via filter.
|
||||
*
|
||||
* @since 3.6.0
|
||||
*/
|
||||
if ( ! apply_filters( 'woocommerce_allow_marketplace_suggestions', true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// An incentive must be available.
|
||||
if ( empty( $this->get_incentive() ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Incentive not manually dismissed.
|
||||
if ( $this->is_incentive_dismissed() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the WooPayments welcome page.
|
||||
*/
|
||||
public function register_payments_welcome_page() {
|
||||
global $menu;
|
||||
|
||||
if ( ! $this->must_be_visible() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Manually dismissed.
|
||||
if ( get_option( 'wc_calypso_bridge_payments_dismissed', 'no' ) === 'yes' ) {
|
||||
return;
|
||||
}
|
||||
$menu_icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4NTIiIGhlaWdodD0iNjg0Ij48cGF0aCBmaWxsPSIjYTJhYWIyIiBkPSJNODIgODZ2NTEyaDY4NFY4NlptMCA1OThjLTQ4IDAtODQtMzgtODQtODZWODZDLTIgMzggMzQgMCA4MiAwaDY4NGM0OCAwIDg0IDM4IDg0IDg2djUxMmMwIDQ4LTM2IDg2LTg0IDg2em0zODQtNTU2djQ0aDg2djg0SDM4MnY0NGgxMjhjMjQgMCA0MiAxOCA0MiA0MnYxMjhjMCAyNC0xOCA0Mi00MiA0MmgtNDR2NDRoLTg0di00NGgtODZ2LTg0aDE3MHYtNDRIMzM4Yy0yNCAwLTQyLTE4LTQyLTQyVjIxNGMwLTI0IDE4LTQyIDQyLTQyaDQ0di00NHoiLz48L3N2Zz4=';
|
||||
|
||||
// Users must be in the experiment.
|
||||
if ( ! $this->is_user_in_treatment_mode() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$menu_icon = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgdmVyc2lvbj0iMS4xIgogICBpZD0ic3ZnNjciCiAgIHNvZGlwb2RpOmRvY25hbWU9IndjcGF5X21lbnVfaWNvbi5zdmciCiAgIHdpZHRoPSI4NTIiCiAgIGhlaWdodD0iNjg0IgogICBpbmtzY2FwZTp2ZXJzaW9uPSIxLjEgKGM0ZThmOWUsIDIwMjEtMDUtMjQpIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM3MSIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzY5IgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VjaGVja2VyYm9hcmQ9IjAiCiAgICAgc2hvd2dyaWQ9ImZhbHNlIgogICAgIGZpdC1tYXJnaW4tdG9wPSIwIgogICAgIGZpdC1tYXJnaW4tbGVmdD0iMCIKICAgICBmaXQtbWFyZ2luLXJpZ2h0PSIwIgogICAgIGZpdC1tYXJnaW4tYm90dG9tPSIwIgogICAgIGlua3NjYXBlOnpvb209IjI1NiIKICAgICBpbmtzY2FwZTpjeD0iLTg0Ljg1NzQyMiIKICAgICBpbmtzY2FwZTpjeT0iLTgzLjI5NDkyMiIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEzMTIiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTA4MSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMTE2IgogICAgIGlua3NjYXBlOndpbmRvdy15PSIyMDIiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMCIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJzdmc2NyIgLz4KICA8cGF0aAogICAgIHRyYW5zZm9ybT0ic2NhbGUoLTEsIDEpIHRyYW5zbGF0ZSgtODUwLCAwKSIKICAgICBkPSJNIDc2OCw4NiBWIDU5OCBIIDg0IFYgODYgWiBtIDAsNTk4IGMgNDgsMCA4NCwtMzggODQsLTg2IFYgODYgQyA4NTIsMzggODE2LDAgNzY4LDAgSCA4NCBDIDM2LDAgMCwzOCAwLDg2IHYgNTEyIGMgMCw0OCAzNiw4NiA4NCw4NiB6IE0gMzg0LDEyOCB2IDQ0IGggLTg2IHYgODQgaCAxNzAgdiA0NCBIIDM0MCBjIC0yNCwwIC00MiwxOCAtNDIsNDIgdiAxMjggYyAwLDI0IDE4LDQyIDQyLDQyIGggNDQgdiA0NCBoIDg0IHYgLTQ0IGggODYgViA0MjggSCAzODQgdiAtNDQgaCAxMjggYyAyNCwwIDQyLC0xOCA0MiwtNDIgViAyMTQgYyAwLC0yNCAtMTgsLTQyIC00MiwtNDIgaCAtNDQgdiAtNDQgeiIKICAgICBmaWxsPSIjYTJhYWIyIgogICAgIGlkPSJwYXRoNjUiIC8+Cjwvc3ZnPgo=';
|
||||
|
||||
$menu_data = array(
|
||||
$menu_data = [
|
||||
'id' => 'wc-calypso-bridge-payments-welcome-page',
|
||||
'title' => __( 'Payments', 'woocommerce' ),
|
||||
'title' => esc_html__( 'Payments', 'woocommerce' ),
|
||||
'path' => '/wc-pay-welcome-page',
|
||||
'position' => '56',
|
||||
'nav_args' => [
|
||||
'title' => __( 'WooCommerce Payments', 'woocommerce' ),
|
||||
'title' => esc_html__( 'WooPayments', 'woocommerce' ),
|
||||
'is_category' => false,
|
||||
'menuId' => 'plugins',
|
||||
'is_top_level' => true,
|
||||
],
|
||||
'icon' => $menu_icon,
|
||||
);
|
||||
];
|
||||
|
||||
wc_admin_register_page( $menu_data );
|
||||
|
||||
@@ -134,71 +120,332 @@ class WcPayWelcomePage {
|
||||
// We need to register this menu via add_menu_page so that it doesn't become a child of
|
||||
// WooCommerce menu.
|
||||
if ( get_option( 'woocommerce_navigation_enabled', 'no' ) === 'yes' ) {
|
||||
$menu_with_nav_data = array(
|
||||
__( 'Payments', 'woocommerce' ),
|
||||
__( 'Payments', 'woocommerce' ),
|
||||
$menu_with_nav_data = [
|
||||
esc_html__( 'Payments', 'woocommerce' ),
|
||||
esc_html__( 'Payments', 'woocommerce' ),
|
||||
'view_woocommerce_reports',
|
||||
'admin.php?page=wc-admin&path=/wc-pay-welcome-page',
|
||||
null,
|
||||
$menu_icon,
|
||||
56,
|
||||
);
|
||||
];
|
||||
|
||||
call_user_func_array( 'add_menu_page', $menu_with_nav_data );
|
||||
}
|
||||
|
||||
// Add badge.
|
||||
$badge = ' <span class="wcpay-menu-badge awaiting-mod count-1"><span class="plugin-count">1</span></span>';
|
||||
foreach ( $menu as $index => $menu_item ) {
|
||||
if ( 'wc-admin&path=/wc-pay-welcome-page' === $menu_item[2]
|
||||
|| 'admin.php?page=wc-admin&path=/wc-pay-welcome-page' === $menu_item[2] ) {
|
||||
//phpcs:ignore
|
||||
$menu[ $index ][0] .= ' <span class="wcpay-menu-badge awaiting-mod count-1"><span class="plugin-count">1</span></span>';
|
||||
// Only add the badge markup if not already present and the menu item is the WooPayments menu item.
|
||||
if ( false === strpos( $menu_item[0], $badge )
|
||||
&& ( 'wc-admin&path=/wc-pay-welcome-page' === $menu_item[2]
|
||||
|| 'admin.php?page=wc-admin&path=/wc-pay-welcome-page' === $menu_item[2] )
|
||||
) {
|
||||
$menu[ $index ][0] .= $badge; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
|
||||
// One menu item with a badge is more than enough.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a WCPay account exists. By checking account data cache.
|
||||
* Adds shared settings for the WooPayments incentive.
|
||||
*
|
||||
* @param array $settings Shared settings.
|
||||
* @return array
|
||||
*/
|
||||
public function shared_settings( $settings ): array {
|
||||
// Return early if not on a wc-admin powered page.
|
||||
if ( ! PageController::is_admin_page() ) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
// Return early if the incentive must not be visible.
|
||||
if ( ! $this->must_be_visible() ) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
$settings['wcpayWelcomePageIncentive'] = $this->get_incentive();
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds allowed promo notes from the WooPayments incentive.
|
||||
*
|
||||
* @param array $promo_notes Allowed promo notes.
|
||||
* @return array
|
||||
*/
|
||||
public function allowed_promo_notes( $promo_notes = [] ): array {
|
||||
// Return early if the incentive must not be visible.
|
||||
if ( ! $this->must_be_visible() ) {
|
||||
return $promo_notes;
|
||||
}
|
||||
|
||||
// Add our incentive ID to the promo notes.
|
||||
$promo_notes[] = $this->get_incentive()['id'];
|
||||
|
||||
return $promo_notes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the WooPayments payment gateway is active and set up or was at some point,
|
||||
* or there are orders processed with it, at some moment.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function has_wcpay_account(): bool {
|
||||
$account_data = get_option( 'wcpay_account_data' );
|
||||
return isset( $account_data['data'] ) && is_array( $account_data['data'] ) && ! empty( $account_data['data'] );
|
||||
private function has_wcpay(): bool {
|
||||
// First, get the stored value, if it exists.
|
||||
// This way we avoid costly DB queries and API calls.
|
||||
// Basically, we only want to know if WooPayments was in use in the past.
|
||||
// Since the past can't be changed, neither can this value.
|
||||
$had_wcpay = get_option( self::HAD_WCPAY_OPTION_NAME );
|
||||
if ( false !== $had_wcpay ) {
|
||||
return $had_wcpay === 'yes';
|
||||
}
|
||||
|
||||
// We need to determine the value.
|
||||
// Start with the assumption that the store didn't have WooPayments in use.
|
||||
$had_wcpay = false;
|
||||
|
||||
// We consider the store to have WooPayments if there is meaningful account data in the WooPayments account cache.
|
||||
// This implies that WooPayments was active at some point and that it was connected.
|
||||
// If WooPayments is active right now, we will not get to this point since the plugin is active check is done first.
|
||||
if ( $this->has_wcpay_account_data() ) {
|
||||
$had_wcpay = true;
|
||||
}
|
||||
|
||||
// If there is at least one order processed with WooPayments, we consider the store to have WooPayments.
|
||||
if ( false === $had_wcpay && ! empty(
|
||||
wc_get_orders(
|
||||
[
|
||||
'payment_method' => 'woocommerce_payments',
|
||||
'return' => 'ids',
|
||||
'limit' => 1,
|
||||
]
|
||||
)
|
||||
) ) {
|
||||
$had_wcpay = true;
|
||||
}
|
||||
|
||||
// Store the value for future use.
|
||||
update_option( self::HAD_WCPAY_OPTION_NAME, $had_wcpay ? 'yes' : 'no' );
|
||||
|
||||
return $had_wcpay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if user is in the experiment.
|
||||
* Check if the WooPayments plugin is active.
|
||||
*
|
||||
* @return bool Whether the user is in the treatment group.
|
||||
* @return boolean
|
||||
*/
|
||||
private function is_user_in_treatment_mode() {
|
||||
$anon_id = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : '';
|
||||
$allow_tracking = get_option( 'woocommerce_allow_tracking' ) === 'yes';
|
||||
$abtest = new \WooCommerce\Admin\Experimental_Abtest(
|
||||
$anon_id,
|
||||
'woocommerce',
|
||||
$allow_tracking
|
||||
);
|
||||
|
||||
return $abtest->get_variation( self::EXPERIMENT_NAME ) === 'treatment';
|
||||
private function is_wcpay_active(): bool {
|
||||
return class_exists( '\WC_Payments' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is another payment gateway installed using a static list of US gateways from WC Store.
|
||||
* Check if there is meaningful data in the WooPayments account cache.
|
||||
*
|
||||
* @return bool Whether there is another payment gateway installed.
|
||||
* @return boolean
|
||||
*/
|
||||
private function is_another_payment_gateway_installed() {
|
||||
$available_gateways = wp_list_pluck( WC()->payment_gateways()->get_available_payment_gateways(), 'id' );
|
||||
|
||||
foreach ( $available_gateways as $gateway ) {
|
||||
if ( in_array( $gateway, self::OTHER_GATEWAYS, true ) ) {
|
||||
return true;
|
||||
}
|
||||
private function has_wcpay_account_data(): bool {
|
||||
$account_data = get_option( 'wcpay_account_data', [] );
|
||||
if ( ! empty( $account_data['data']['account_id'] ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current incentive has been manually dismissed.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function is_incentive_dismissed(): bool {
|
||||
$dismissed_incentives = get_option( 'wcpay_welcome_page_incentives_dismissed', [] );
|
||||
|
||||
// If there are no dismissed incentives, return early.
|
||||
if ( empty( $dismissed_incentives ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return early if there is no eligible incentive.
|
||||
$incentive = $this->get_incentive();
|
||||
if ( empty( $incentive ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Search the incentive ID in the dismissed incentives list.
|
||||
if ( in_array( $incentive['id'], $dismissed_incentives, true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and caches eligible incentive from the WooPayments API.
|
||||
*
|
||||
* @return array|null Array of eligible incentive or null.
|
||||
*/
|
||||
private function get_incentive(): ?array {
|
||||
// Return in-memory cached incentive if it is set.
|
||||
if ( isset( $this->incentive ) ) {
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
// Get the cached data.
|
||||
$cache = get_transient( self::CACHE_TRANSIENT_NAME );
|
||||
|
||||
// If the cached data is not expired and it's a WP_Error,
|
||||
// it means there was an API error previously and we should not retry just yet.
|
||||
if ( is_wp_error( $cache ) ) {
|
||||
// Initialize the in-memory cache and return it.
|
||||
$this->incentive = [];
|
||||
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
// Gather the store context data.
|
||||
$store_context = [
|
||||
// Store ISO-2 country code, e.g. `US`.
|
||||
'country' => WC()->countries->get_base_country(),
|
||||
// Store locale, e.g. `en_US`.
|
||||
'locale' => get_locale(),
|
||||
// WooCommerce active for duration in seconds.
|
||||
'active_for' => WCAdminHelper::get_wcadmin_active_for_in_seconds(),
|
||||
// Whether the store has paid orders in the last 90 days.
|
||||
'has_orders' => ! empty(
|
||||
wc_get_orders(
|
||||
[
|
||||
'status' => [ 'wc-completed', 'wc-processing' ],
|
||||
'date_created' => '>=' . strtotime( '-90 days' ),
|
||||
'return' => 'ids',
|
||||
'limit' => 1,
|
||||
]
|
||||
)
|
||||
),
|
||||
// Whether the store has at least one payment gateway enabled.
|
||||
'has_payments' => ! empty( WC()->payment_gateways()->get_available_payment_gateways() ),
|
||||
'has_wcpay' => $this->has_wcpay(),
|
||||
];
|
||||
|
||||
// Fingerprint the store context through a hash of certain entries.
|
||||
$store_context_hash = $this->generate_context_hash( $store_context );
|
||||
|
||||
// Use the transient cached incentive if it exists, it is not expired,
|
||||
// and the store context hasn't changed since we last requested from the WooPayments API (based on context hash).
|
||||
if ( false !== $cache
|
||||
&& ! empty( $cache['context_hash'] ) && is_string( $cache['context_hash'] )
|
||||
&& hash_equals( $store_context_hash, $cache['context_hash'] ) ) {
|
||||
|
||||
// We have a store context hash and it matches with the current context one.
|
||||
// We can use the cached incentive data.
|
||||
// Store the incentive in the in-memory cache and return it.
|
||||
$this->incentive = $cache['incentive'] ?? [];
|
||||
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
// By this point, we have an expired transient or the store context has changed.
|
||||
// Query for incentives by calling the WooPayments API.
|
||||
$url = add_query_arg(
|
||||
$store_context,
|
||||
'https://public-api.wordpress.com/wpcom/v2/wcpay/incentives',
|
||||
);
|
||||
|
||||
$response = wp_remote_get(
|
||||
$url,
|
||||
[
|
||||
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
|
||||
]
|
||||
);
|
||||
|
||||
// Return early if there is an error, waiting 6 hours before the next attempt.
|
||||
if ( is_wp_error( $response ) ) {
|
||||
// Store a trimmed down, lightweight error.
|
||||
$error = new \WP_Error(
|
||||
$response->get_error_code(),
|
||||
$response->get_error_message(),
|
||||
wp_remote_retrieve_response_code( $response )
|
||||
);
|
||||
// Store the error in the transient so we know this is due to an API error.
|
||||
set_transient( self::CACHE_TRANSIENT_NAME, $error, HOUR_IN_SECONDS * 6 );
|
||||
// Initialize the in-memory cache and return it.
|
||||
$this->incentive = [];
|
||||
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
$cache_for = wp_remote_retrieve_header( $response, 'cache-for' );
|
||||
// Initialize the in-memory cache.
|
||||
$this->incentive = [];
|
||||
|
||||
if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
|
||||
// Decode the results, falling back to an empty array.
|
||||
$results = json_decode( wp_remote_retrieve_body( $response ), true ) ?? [];
|
||||
|
||||
// Find all `welcome_page` incentives.
|
||||
$incentives = array_filter(
|
||||
$results,
|
||||
function( $incentive ) {
|
||||
return 'welcome_page' === $incentive['type'];
|
||||
}
|
||||
);
|
||||
|
||||
// Use the first found matching incentive or empty array if none was found.
|
||||
// Store incentive in the in-memory cache.
|
||||
$this->incentive = empty( $incentives ) ? [] : reset( $incentives );
|
||||
}
|
||||
|
||||
// Skip transient cache if `cache-for` header equals zero.
|
||||
if ( '0' === $cache_for ) {
|
||||
// If we have a transient cache that is not expired, delete it so there are no leftovers.
|
||||
if ( false !== $cache ) {
|
||||
delete_transient( self::CACHE_TRANSIENT_NAME );
|
||||
}
|
||||
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
// Store incentive in transient cache (together with the context hash) for the given number of seconds
|
||||
// or 1 day in seconds. Also attach a timestamp to the transient data so we know when we last fetched.
|
||||
set_transient(
|
||||
self::CACHE_TRANSIENT_NAME,
|
||||
[
|
||||
'incentive' => $this->incentive,
|
||||
'context_hash' => $store_context_hash,
|
||||
'timestamp' => time(),
|
||||
],
|
||||
! empty( $cache_for ) ? (int) $cache_for : DAY_IN_SECONDS
|
||||
);
|
||||
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash from the store context data.
|
||||
*
|
||||
* @param array $context The store context data.
|
||||
*
|
||||
* @return string The context hash.
|
||||
*/
|
||||
private function generate_context_hash( array $context ): string {
|
||||
// Include only certain entries in the context hash.
|
||||
// We need only discrete, user-interaction dependent data.
|
||||
// Entries like `active_for` have no place in the hash generation since they change automatically.
|
||||
return md5(
|
||||
wp_json_encode(
|
||||
[
|
||||
'country' => $context['country'] ?? '',
|
||||
'locale' => $context['locale'] ?? '',
|
||||
'has_orders' => $context['has_orders'] ?? false,
|
||||
'has_payments' => $context['has_payments'] ?? false,
|
||||
'has_wcpay' => $context['has_wcpay'] ?? false,
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user