Merged in feature/MAW-855-import-code-into-aws (pull request #2)

code import from pantheon

* code import from pantheon
This commit is contained in:
Tony Volpe
2023-12-04 23:08:14 +00:00
parent 8c9b1312bc
commit 8f4b5efda6
4766 changed files with 185592 additions and 239967 deletions

View File

@@ -0,0 +1,117 @@
<?php
namespace Automattic\WooCommerce\Blocks\AI;
use Automattic\Jetpack\Config;
use Automattic\Jetpack\Connection\Manager;
use Automattic\Jetpack\Connection\Utils;
/**
* Class Configuration
*/
class Configuration {
/**
* The name of the option that stores the site owner's consent to connect to the AI API.
*
* @var string
*/
private $consent_option_name = 'woocommerce_blocks_allow_ai_connection';
/**
* The Jetpack connection manager.
*
* @var Manager
*/
private $manager;
/**
* The Jetpack configuration.
*
* @var Config
*/
private $config;
/**
* Configuration constructor.
*/
public function __construct() {
if ( ! class_exists( 'Automattic\Jetpack\Connection\Manager' ) || ! class_exists( 'Automattic\Jetpack\Config' ) ) {
return;
}
$this->manager = new Manager( 'woocommerce_blocks' );
$this->config = new Config();
}
/**
* Initialize the site and user connection and registration.
*
* @return bool|\WP_Error
*/
public function init() {
if ( ! $this->should_connect() ) {
return false;
}
$this->enable_connection_feature();
return $this->register_and_connect();
}
/**
* Verify if the site should connect to Jetpack.
*
* @return bool
*/
private function should_connect() {
$site_owner_consent = get_option( $this->consent_option_name );
return $site_owner_consent && class_exists( 'Automattic\Jetpack\Connection\Utils' ) && class_exists( 'Automattic\Jetpack\Connection\Manager' );
}
/**
* Initialize Jetpack's connection feature within the WooCommerce Blocks plugin.
*
* @return void
*/
private function enable_connection_feature() {
$this->config->ensure(
'connection',
array(
'slug' => 'woocommerce/woocommerce-blocks',
'name' => 'WooCommerce Blocks',
)
);
}
/**
* Register the site with Jetpack.
*
* @return bool|\WP_Error
*/
private function register_and_connect() {
Utils::init_default_constants();
$jetpack_id = \Jetpack_Options::get_option( 'id' );
$jetpack_public = \Jetpack_Options::get_option( 'public' );
$register = $jetpack_id && $jetpack_public ? true : $this->manager->register();
if ( true === $register && ! $this->manager->is_user_connected() ) {
$this->manager->connect_user();
return true;
}
return false;
}
/**
* Unregister the site with Jetpack.
*
* @return void
*/
private function unregister_site() {
if ( $this->manager->is_connected() ) {
$this->manager->remove_connection();
}
}
}

View File

@@ -0,0 +1,150 @@
<?php
namespace Automattic\WooCommerce\Blocks\AI;
use Automattic\Jetpack\Connection\Client;
use Jetpack_Options;
use WP_Error;
use WpOrg\Requests\Exception;
use WpOrg\Requests\Requests;
/**
* Class Connection
*/
class Connection {
const TEXT_COMPLETION_API_URL = 'https://public-api.wordpress.com/wpcom/v2/text-completion';
/**
* The post request.
*
* @param string $token The JWT token.
* @param string $prompt The prompt to send to the API.
* @param int $timeout The timeout for the request.
*
* @return mixed
*/
public function fetch_ai_response( $token, $prompt, $timeout = 15 ) {
if ( $token instanceof \WP_Error ) {
return $token;
}
$response = wp_remote_post(
self::TEXT_COMPLETION_API_URL,
array(
'body' =>
array(
'feature' => 'woocommerce_blocks_patterns',
'prompt' => $prompt,
'token' => $token,
),
'timeout' => $timeout,
)
);
if ( is_wp_error( $response ) ) {
return new \WP_Error( $response->get_error_code(), esc_html__( 'Failed to connect with the AI endpoint: try again later.', 'woocommerce' ), $response->get_error_message() );
}
$body = wp_remote_retrieve_body( $response );
return json_decode( $body, true );
}
/**
* Fetch the AI responses in parallel using the given token and prompts.
*
* @param string $token The JWT token.
* @param array $prompts The prompts to send to the API.
* @param int $timeout The timeout for the request.
*
* @return array|WP_Error The responses or a WP_Error object.
*/
public function fetch_ai_responses( $token, array $prompts, $timeout = 15 ) {
if ( $token instanceof \WP_Error ) {
return $token;
}
$requests = array();
foreach ( $prompts as $prompt ) {
$requests[] = array(
'url' => self::TEXT_COMPLETION_API_URL,
'type' => 'POST',
'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ),
'data' => wp_json_encode(
array(
'feature' => 'woocommerce_blocks_patterns',
'prompt' => $prompt,
'token' => $token,
)
),
);
}
$responses = Requests::request_multiple( $requests, array( 'timeout' => $timeout ) );
$processed_responses = array();
foreach ( $responses as $key => $response ) {
if ( is_wp_error( $response ) || is_a( $response, Exception::class ) ) {
$processed_responses[ $key ] = null;
continue;
}
$processed_responses[ $key ] = json_decode( $response->body, true );
}
return $processed_responses;
}
/**
* Return the site ID.
*
* @return integer|\WP_Error The site ID or a WP_Error object.
*/
public function get_site_id() {
if ( ! class_exists( Jetpack_Options::class ) ) {
return new \WP_Error( 'site-id-error', esc_html__( 'Failed to fetch the site ID: try again later.', 'woocommerce' ) );
}
$site_id = Jetpack_Options::get_option( 'id' );
if ( ! $site_id ) {
return new \WP_Error( 'site-id-error', esc_html__( 'Failed to fetch the site ID: The site is not registered.', 'woocommerce' ) );
}
return $site_id;
}
/**
* Fetch the JWT token.
*
* @param integer $site_id The site ID.
*
* @return string|\WP_Error The JWT token or a WP_Error object.
*/
public function get_jwt_token( $site_id ) {
if ( is_wp_error( $site_id ) ) {
return $site_id;
}
$request = Client::wpcom_json_api_request_as_user(
sprintf( '/sites/%d/jetpack-openai-query/jwt', $site_id ),
'2',
array(
'method' => 'POST',
'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ),
)
);
$response = json_decode( wp_remote_retrieve_body( $request ) );
if ( $response instanceof \WP_Error ) {
return new \WP_Error( $response->get_error_code(), esc_html__( 'Failed to generate the JWT token', 'woocommerce' ), $response->get_error_message() );
}
if ( ! isset( $response->token ) ) {
return new \WP_Error( 'failed-to-retrieve-jwt-token', esc_html__( 'Failed to retrieve the JWT token: Try again later.', 'woocommerce' ) );
}
return $response->token;
}
}

View File

@@ -18,6 +18,34 @@ class Api {
*/
private $inline_scripts = [];
/**
* Determines if caching is enabled for script data.
*
* @var boolean
*/
private $disable_cache = false;
/**
* Stores loaded script data for the current request
*
* @var array|null
*/
private $script_data = null;
/**
* Stores the hash for the script data, made up of the site url, plugin version and package path.
*
* @var string
*/
private $script_data_hash;
/**
* Stores the transient key used to cache the script data. This will change if the site is accessed via HTTPS or HTTP.
*
* @var string
*/
private $script_data_transient_key = 'woocommerce_blocks_asset_api_script_data';
/**
* Reference to the Package instance
*
@@ -31,7 +59,18 @@ class Api {
* @param Package $package An instance of Package.
*/
public function __construct( Package $package ) {
$this->package = $package;
$this->package = $package;
$this->disable_cache = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) || ! $this->package->feature()->is_production_environment();
// If the site is accessed via HTTPS, change the transient key. This is to prevent the script URLs being cached
// with the first scheme they are accessed on after cache expiry.
if ( is_ssl() ) {
$this->script_data_transient_key .= '_ssl';
}
if ( ! $this->disable_cache ) {
$this->script_data_hash = $this->get_script_data_hash();
}
add_action( 'shutdown', array( $this, 'update_script_data_cache' ), 20 );
}
/**
@@ -76,6 +115,64 @@ class Api {
return $path_to_metadata_from_plugin_root;
}
/**
* Generates a hash containing the site url, plugin version and package path.
*
* Moving the plugin, changing the version, or changing the site url will result in a new hash and the cache will be invalidated.
*
* @return string The generated hash.
*/
private function get_script_data_hash() {
return md5( get_option( 'siteurl', '' ) . $this->package->get_version() . $this->package->get_path() );
}
/**
* Initialize and load cached script data from the transient cache.
*
* @return array
*/
private function get_cached_script_data() {
if ( $this->disable_cache ) {
return [];
}
$transient_value = json_decode( (string) get_transient( $this->script_data_transient_key ), true );
if (
json_last_error() !== JSON_ERROR_NONE ||
empty( $transient_value ) ||
empty( $transient_value['script_data'] ) ||
empty( $transient_value['version'] ) ||
$transient_value['version'] !== $this->package->get_version() ||
empty( $transient_value['hash'] ) ||
$transient_value['hash'] !== $this->script_data_hash
) {
return [];
}
return (array) ( $transient_value['script_data'] ?? [] );
}
/**
* Store all cached script data in the transient cache.
*/
public function update_script_data_cache() {
if ( is_null( $this->script_data ) || $this->disable_cache ) {
return;
}
set_transient(
$this->script_data_transient_key,
wp_json_encode(
array(
'script_data' => $this->script_data,
'version' => $this->package->get_version(),
'hash' => $this->script_data_hash,
)
),
DAY_IN_SECONDS * 30
);
}
/**
* Get src, version and dependencies given a script relative src.
*
@@ -85,31 +182,37 @@ class Api {
* @return array src, version and dependencies of the script.
*/
public function get_script_data( $relative_src, $dependencies = [] ) {
$src = '';
$version = '1';
if ( $relative_src ) {
$src = $this->get_asset_url( $relative_src );
$asset_path = $this->package->get_path(
str_replace( '.js', '.asset.php', $relative_src )
if ( ! $relative_src ) {
return array(
'src' => '',
'version' => '1',
'dependencies' => $dependencies,
);
if ( file_exists( $asset_path ) ) {
// The following require is safe because we are checking if the file exists and it is not a user input.
// nosemgrep audit.php.lang.security.file.inclusion-arg.
$asset = require $asset_path;
$dependencies = isset( $asset['dependencies'] ) ? array_merge( $asset['dependencies'], $dependencies ) : $dependencies;
$version = ! empty( $asset['version'] ) ? $asset['version'] : $this->get_file_version( $relative_src );
} else {
$version = $this->get_file_version( $relative_src );
}
}
return array(
'src' => $src,
'version' => $version,
'dependencies' => $dependencies,
);
if ( is_null( $this->script_data ) ) {
$this->script_data = $this->get_cached_script_data();
}
if ( empty( $this->script_data[ $relative_src ] ) ) {
$asset_path = $this->package->get_path( str_replace( '.js', '.asset.php', $relative_src ) );
// The following require is safe because we are checking if the file exists and it is not a user input.
// nosemgrep audit.php.lang.security.file.inclusion-arg.
$asset = file_exists( $asset_path ) ? require $asset_path : [];
$this->script_data[ $relative_src ] = array(
'src' => $this->get_asset_url( $relative_src ),
'version' => ! empty( $asset['version'] ) ? $asset['version'] : $this->get_file_version( $relative_src ),
'dependencies' => ! empty( $asset['dependencies'] ) ? $asset['dependencies'] : [],
);
}
// Return asset details as well as the requested dependencies array.
return [
'src' => $this->script_data[ $relative_src ]['src'],
'version' => $this->script_data[ $relative_src ]['version'],
'dependencies' => array_merge( $this->script_data[ $relative_src ]['dependencies'], $dependencies ),
];
}
/**

View File

@@ -2,7 +2,7 @@
namespace Automattic\WooCommerce\Blocks\Assets;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
use Exception;
use InvalidArgumentException;
@@ -85,13 +85,14 @@ class AssetDataRegistry {
'currency' => $this->get_currency_data(),
'currentUserId' => get_current_user_id(),
'currentUserIsAdmin' => current_user_can( 'manage_woocommerce' ),
'dateFormat' => wc_date_format(),
'homeUrl' => esc_url( home_url( '/' ) ),
'locale' => $this->get_locale_data(),
'dashboardUrl' => wc_get_account_endpoint_url( 'dashboard' ),
'orderStatuses' => $this->get_order_statuses(),
'placeholderImgSrc' => wc_placeholder_img_src(),
'productsSettings' => $this->get_products_settings(),
'siteTitle' => get_bloginfo( 'name' ),
'siteTitle' => wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ),
'storePages' => $this->get_store_pages(),
'wcAssetUrl' => plugins_url( 'assets/', WC_PLUGIN_FILE ),
'wcVersion' => defined( 'WC_VERSION' ) ? WC_VERSION : '',
@@ -293,9 +294,9 @@ class AssetDataRegistry {
* You can only register data that is not already in the registry identified by the given key. If there is a
* duplicate found, unless $ignore_duplicates is true, an exception will be thrown.
*
* @param string $key The key used to reference the data being registered.
* @param mixed $data If not a function, registered to the registry as is. If a function, then the
* callback is invoked right before output to the screen.
* @param string $key The key used to reference the data being registered. This should use camelCase.
* @param mixed $data If not a function, registered to the registry as is. If a function, then the
* callback is invoked right before output to the screen.
* @param boolean $check_key_exists If set to true, duplicate data will be ignored if the key exists.
* If false, duplicate data will cause an exception.
*
@@ -317,16 +318,40 @@ class AssetDataRegistry {
}
/**
* Hydrate from API.
* Hydrate from the API.
*
* @param string $path REST API path to preload.
*/
public function hydrate_api_request( $path ) {
if ( ! isset( $this->preloaded_api_requests[ $path ] ) ) {
$this->preloaded_api_requests = rest_preload_api_request( $this->preloaded_api_requests, $path );
$this->preloaded_api_requests[ $path ] = Package::container()->get( Hydration::class )->get_rest_api_response_data( $path );
}
}
/**
* Hydrate some data from the API.
*
* @param string $key The key used to reference the data being registered.
* @param string $path REST API path to preload.
* @param boolean $check_key_exists If set to true, duplicate data will be ignored if the key exists.
* If false, duplicate data will cause an exception.
*
* @throws InvalidArgumentException Only throws when site is in debug mode. Always logs the error.
*/
public function hydrate_data_from_api_request( $key, $path, $check_key_exists = false ) {
$this->add(
$key,
function() use ( $path ) {
if ( isset( $this->preloaded_api_requests[ $path ], $this->preloaded_api_requests[ $path ]['body'] ) ) {
return $this->preloaded_api_requests[ $path ]['body'];
}
$response = Package::container()->get( Hydration::class )->get_rest_api_response_data( $path );
return $response['body'] ?? '';
},
$check_key_exists
);
}
/**
* Adds a page permalink to the data registry.
*

View File

@@ -62,6 +62,7 @@ final class AssetsController {
$this->api->register_script( 'wc-price-format', 'build/price-format.js', [], false );
$this->api->register_script( 'wc-blocks-checkout', 'build/blocks-checkout.js', [] );
$this->api->register_script( 'wc-blocks-components', 'build/blocks-components.js', [] );
wp_add_inline_script(
'wc-blocks-middleware',

View File

@@ -1,10 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Blocks\AI\Connection;
use Automattic\WooCommerce\Blocks\Images\Pexels;
use Automattic\WooCommerce\Blocks\Domain\Package;
use Automattic\WooCommerce\Blocks\Patterns\PatternUpdater;
use Automattic\WooCommerce\Blocks\Patterns\ProductUpdater;
/**
* Registers patterns under the `./patterns/` directory.
* Registers patterns under the `./patterns/` directory and updates their content.
* Each pattern is defined as a PHP file and defines its metadata using plugin-style headers.
* The minimum required definition is:
*
@@ -48,6 +52,36 @@ class BlockPatterns {
$this->patterns_path = $package->get_path( 'patterns' );
add_action( 'init', array( $this, 'register_block_patterns' ) );
add_action( 'update_option_woo_ai_describe_store_description', array( $this, 'schedule_on_option_update' ), 10, 2 );
add_action( 'update_option_woo_ai_describe_store_description', array( $this, 'update_ai_connection_allowed_option' ), 10, 2 );
add_action( 'upgrader_process_complete', array( $this, 'schedule_on_plugin_update' ), 10, 2 );
add_action( 'woocommerce_update_patterns_content', array( $this, 'update_patterns_content' ) );
}
/**
* Make sure the 'woocommerce_blocks_allow_ai_connection' option is set to true if the site is connected to AI.
*
* @param string $option The option name.
* @param string $value The option value.
*
* @return bool
*/
public function update_ai_connection_allowed_option( $option, $value ): bool {
$ai_connection = new Connection();
$site_id = $ai_connection->get_site_id();
if ( is_wp_error( $site_id ) ) {
return update_option( 'woocommerce_blocks_allow_ai_connection', false );
}
$token = $ai_connection->get_jwt_token( $site_id );
if ( is_wp_error( $token ) ) {
return update_option( 'woocommerce_blocks_allow_ai_connection', false );
}
return update_option( 'woocommerce_blocks_allow_ai_connection', true );
}
/**
@@ -198,4 +232,109 @@ class BlockPatterns {
register_block_pattern( $pattern_data['slug'], $pattern_data );
}
}
/**
* Update the patterns content when the store description is changed.
*
* @param string $option The option name.
* @param string $value The option value.
*/
public function schedule_on_option_update( $option, $value ) {
$this->schedule_patterns_content_update( $value );
}
/**
* Update the patterns content when the WooCommerce Blocks plugin is updated.
*
* @param \WP_Upgrader $upgrader_object WP_Upgrader instance.
* @param array $options Array of bulk item update data.
*/
public function schedule_on_plugin_update( $upgrader_object, $options ) {
if ( 'update' === $options['action'] && 'plugin' === $options['type'] ) {
foreach ( $options['plugins'] as $plugin ) {
if ( str_contains( $plugin, 'woocommerce-gutenberg-products-block.php' ) || str_contains( $plugin, 'woocommerce.php' ) ) {
$business_description = get_option( 'woo_ai_describe_store_description' );
if ( $business_description ) {
$this->schedule_patterns_content_update( $business_description );
}
}
}
}
}
/**
* Update the patterns content when the store description is changed.
*
* @param string $business_description The business description.
*/
public function schedule_patterns_content_update( $business_description ) {
if ( ! class_exists( 'WooCommerce' ) ) {
return;
}
$action_scheduler = WP_PLUGIN_DIR . '/woocommerce/packages/action-scheduler/action-scheduler.php';
if ( ! file_exists( $action_scheduler ) ) {
return;
}
require_once $action_scheduler;
as_schedule_single_action( time(), 'woocommerce_update_patterns_content', array( $business_description ) );
}
/**
* Update the patterns content.
*
* @param string $value The new value saved for the add_option_woo_ai_describe_store_description option.
*
* @return bool|string|\WP_Error
*/
public function update_patterns_content( $value ) {
$allow_ai_connection = get_option( 'woocommerce_blocks_allow_ai_connection' );
if ( ! $allow_ai_connection ) {
return new \WP_Error(
'ai_connection_not_allowed',
__( 'AI content generation is not allowed on this store. Update your store settings if you wish to enable this feature.', 'woocommerce' )
);
}
$ai_connection = new Connection();
$site_id = $ai_connection->get_site_id();
if ( is_wp_error( $site_id ) ) {
return $site_id->get_error_message();
}
$token = $ai_connection->get_jwt_token( $site_id );
if ( is_wp_error( $token ) ) {
return $token->get_error_message();
}
$business_description = get_option( 'woo_ai_describe_store_description' );
$images = ( new Pexels() )->get_images( $ai_connection, $token, $business_description );
if ( is_wp_error( $images ) ) {
return $images->get_error_message();
}
$populate_patterns = ( new PatternUpdater() )->generate_content( $ai_connection, $token, $images, $business_description );
if ( is_wp_error( $populate_patterns ) ) {
return $populate_patterns->get_error_message();
}
$populate_products = ( new ProductUpdater() )->generate_content( $ai_connection, $token, $images, $business_description );
if ( is_wp_error( $populate_products ) ) {
return $populate_products->get_error_message();
}
return true;
}
}

View File

@@ -1,6 +1,7 @@
<?php
namespace Automattic\WooCommerce\Blocks;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Blocks\Domain\Package;
use Automattic\WooCommerce\Blocks\Templates\CartTemplate;
use Automattic\WooCommerce\Blocks\Templates\CheckoutTemplate;
@@ -9,8 +10,8 @@ use Automattic\WooCommerce\Blocks\Templates\ProductSearchResultsTemplate;
use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplateCompatibility;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Blocks\Templates\OrderConfirmationTemplate;
use Automattic\WooCommerce\Blocks\Utils\SettingsUtils;
use \WP_Post;
use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplate;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateMigrationUtils;
/**
* BlockTypesController class.
@@ -19,27 +20,6 @@ use \WP_Post;
*/
class BlockTemplatesController {
/**
* Holds the Package instance
*
* @var Package
*/
private $package;
/**
* Holds the path for the directory where the block templates will be kept.
*
* @var string
*/
private $templates_directory;
/**
* Holds the path for the directory where the block template parts will be kept.
*
* @var string
*/
private $template_parts_directory;
/**
* Directory which contains all templates
*
@@ -47,6 +27,13 @@ class BlockTemplatesController {
*/
const TEMPLATES_ROOT_DIR = 'templates';
/**
* Package instance.
*
* @var Package
*/
private $package;
/**
* Constructor.
*
@@ -55,11 +42,11 @@ class BlockTemplatesController {
public function __construct( Package $package ) {
$this->package = $package;
$feature_gating = $package->feature();
$is_block_templates_controller_refactor_enabled = $feature_gating->is_block_templates_controller_refactor_enabled();
// This feature is gated for WooCommerce versions 6.0.0 and above.
if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '6.0.0', '>=' ) ) {
$root_path = plugin_dir_path( __DIR__ ) . self::TEMPLATES_ROOT_DIR . DIRECTORY_SEPARATOR;
$this->templates_directory = $root_path . BlockTemplateUtils::DIRECTORY_NAMES['TEMPLATES'];
$this->template_parts_directory = $root_path . BlockTemplateUtils::DIRECTORY_NAMES['TEMPLATE_PARTS'];
if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '6.0.0', '>=' ) && ! $is_block_templates_controller_refactor_enabled ) {
$this->init();
}
}
@@ -68,6 +55,7 @@ class BlockTemplatesController {
* Initialization method.
*/
protected function init() {
add_filter( 'default_wp_template_part_areas', array( $this, 'register_mini_cart_template_part_area' ), 10, 1 );
add_action( 'template_redirect', array( $this, 'render_block_template' ) );
add_filter( 'pre_get_block_template', array( $this, 'get_block_template_fallback' ), 10, 3 );
add_filter( 'pre_get_block_file_template', array( $this, 'get_block_file_template' ), 10, 3 );
@@ -79,9 +67,6 @@ class BlockTemplatesController {
if ( wc_current_theme_is_fse_theme() ) {
add_action( 'init', array( $this, 'maybe_migrate_content' ) );
add_filter( 'woocommerce_settings_pages', array( $this, 'template_permalink_settings' ) );
add_filter( 'pre_update_option', array( $this, 'update_template_permalink' ), 10, 2 );
add_action( 'woocommerce_admin_field_permalink', array( SettingsUtils::class, 'permalink_input_field' ) );
// By default, the Template Part Block only supports template parts that are in the current theme directory.
// This render_callback wrapper allows us to add support for plugin-housed template parts.
@@ -113,7 +98,7 @@ class BlockTemplatesController {
$settings['original_render_callback'] = $settings['render_callback'];
$settings['render_callback'] = function( $attributes, $content ) use ( $settings ) {
// The shortcode has already been rendered, so look for the cart/checkout HTML.
if ( strstr( $content, 'woocommerce-cart-form' ) || strstr( $content, 'woocommerce-checkout-form' ) ) {
if ( strstr( $content, 'woocommerce-cart-form' ) || strstr( $content, 'wc-empty-cart-message' ) || strstr( $content, 'woocommerce-checkout-form' ) ) {
// Return early before wpautop runs again.
return $content;
}
@@ -128,9 +113,47 @@ class BlockTemplatesController {
10,
2
);
/**
* Prevents the pages that are assigned as cart/checkout from showing the "template" selector in the page-editor.
* We want to avoid this flow and point users towards the site editor instead.
*/
add_action(
'current_screen',
function () {
if ( ! is_admin() ) {
return;
}
$current_screen = get_current_screen();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $current_screen && 'page' === $current_screen->id && ! empty( $_GET['post'] ) && in_array( absint( $_GET['post'] ), [ wc_get_page_id( 'cart' ), wc_get_page_id( 'checkout' ) ], true ) ) {
wp_add_inline_style( 'wc-blocks-editor-style', '.edit-post-post-template { display: none; }' );
}
},
10
);
}
}
/**
* Add Mini-Cart to the default template part areas.
*
* @param array $default_area_definitions An array of supported area objects.
* @return array The supported template part areas including the Mini-Cart one.
*/
public function register_mini_cart_template_part_area( $default_area_definitions ) {
$mini_cart_template_part_area = [
'area' => 'mini-cart',
'label' => __( 'Mini-Cart', 'woocommerce' ),
'description' => __( 'The Mini-Cart template allows shoppers to see their cart items and provides access to the Cart and Checkout pages.', 'woocommerce' ),
'icon' => 'mini-cart',
'area_tag' => 'mini-cart',
];
return array_merge( $default_area_definitions, [ $mini_cart_template_part_area ] );
}
/**
* Renders the `core/template-part` block on the server.
*
@@ -138,7 +161,7 @@ class BlockTemplatesController {
* @return string The render.
*/
public function render_woocommerce_template_part( $attributes ) {
if ( 'woocommerce/woocommerce' === $attributes['theme'] ) {
if ( isset( $attributes['theme'] ) && 'woocommerce/woocommerce' === $attributes['theme'] ) {
$template_part = BlockTemplateUtils::get_block_template( $attributes['theme'] . '//' . $attributes['slug'], 'wp_template_part' );
if ( $template_part && ! empty( $template_part->content ) ) {
@@ -301,7 +324,7 @@ class BlockTemplatesController {
return $template;
}
$directory = $this->get_templates_directory( $template_type );
$directory = BlockTemplateUtils::get_templates_directory( $template_type );
$template_file_path = $directory . '/' . $template_slug . '.html';
$template_object = BlockTemplateUtils::create_new_block_template_object( $template_file_path, $template_type, $template_slug );
$template_built = BlockTemplateUtils::build_template_result_from_file( $template_object, $template_type );
@@ -323,7 +346,7 @@ class BlockTemplatesController {
* @return array
*/
public function add_block_templates( $query_result, $query, $template_type ) {
if ( ! BlockTemplateUtils::supports_block_templates() ) {
if ( ! BlockTemplateUtils::supports_block_templates( $template_type ) ) {
return $query_result;
}
@@ -410,8 +433,11 @@ class BlockTemplatesController {
}
}
$new_content = SingleProductTemplateCompatibility::add_compatibility_layer( $template->content );
$template->content = $new_content;
if ( post_password_required() ) {
$template->content = SingleProductTemplate::add_password_form( $template->content );
} else {
$template->content = SingleProductTemplateCompatibility::add_compatibility_layer( $template->content );
}
}
}
@@ -456,11 +482,14 @@ class BlockTemplatesController {
* @return array Templates from the WooCommerce blocks plugin directory.
*/
public function get_block_templates_from_woocommerce( $slugs, $already_found_templates, $template_type = 'wp_template' ) {
$directory = $this->get_templates_directory( $template_type );
$directory = BlockTemplateUtils::get_templates_directory( $template_type );
$template_files = BlockTemplateUtils::get_template_paths( $directory );
$templates = array();
foreach ( $template_files as $template_file ) {
if ( ! $this->package->is_experimental_build() && str_contains( $template_file, 'templates/parts/product-gallery.html' ) ) {
break;
}
// Skip the template if it's blockified, and we should only use classic ones.
if ( ! BlockTemplateUtils::should_use_blockified_product_grid_templates() && strpos( $template_file, 'blockified' ) !== false ) {
continue;
@@ -539,25 +568,6 @@ class BlockTemplatesController {
return BlockTemplateUtils::filter_block_templates_by_feature_flag( $templates );
}
/**
* Gets the directory where templates of a specific template type can be found.
*
* @param string $template_type wp_template or wp_template_part.
*
* @return string
*/
protected function get_templates_directory( $template_type = 'wp_template' ) {
if ( 'wp_template_part' === $template_type ) {
return $this->template_parts_directory;
}
if ( BlockTemplateUtils::should_use_blockified_product_grid_templates() ) {
return $this->templates_directory . '/blockified';
}
return $this->templates_directory;
}
/**
* Returns the path of a template on the Blocks template folder.
*
@@ -567,7 +577,7 @@ class BlockTemplatesController {
* @return string
*/
public function get_template_path_from_woocommerce( $template_slug, $template_type = 'wp_template' ) {
return $this->get_templates_directory( $template_type ) . '/' . $template_slug . '.html';
return BlockTemplateUtils::get_templates_directory( $template_type ) . '/' . $template_slug . '.html';
}
/**
@@ -582,7 +592,7 @@ class BlockTemplatesController {
if ( ! $template_name ) {
return false;
}
$directory = $this->get_templates_directory( $template_type ) . '/' . $template_name . '.html';
$directory = BlockTemplateUtils::get_templates_directory( $template_type ) . '/' . $template_name . '.html';
return is_readable(
$directory
@@ -600,7 +610,13 @@ class BlockTemplatesController {
if (
is_singular( 'product' ) && $this->block_template_is_available( 'single-product' )
) {
$templates = get_block_templates( array( 'slug__in' => array( 'single-product' ) ) );
global $post;
$valid_slugs = [ 'single-product' ];
if ( 'product' === $post->post_type && $post->post_name ) {
$valid_slugs[] = 'single-product-' . $post->post_name;
}
$templates = get_block_templates( array( 'slug__in' => $valid_slugs ) );
if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {
add_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
@@ -665,12 +681,6 @@ class BlockTemplatesController {
! BlockTemplateUtils::theme_has_template( CheckoutTemplate::get_slug() ) && $this->block_template_is_available( CheckoutTemplate::get_slug() )
) {
add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 );
} elseif (
is_wc_endpoint_url( 'order-received' )
&& ! BlockTemplateUtils::theme_has_template( OrderConfirmationTemplate::get_slug() )
&& $this->block_template_is_available( OrderConfirmationTemplate::get_slug() )
) {
add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 );
} else {
$queried_object = get_queried_object();
if ( is_null( $queried_object ) ) {
@@ -742,214 +752,17 @@ class BlockTemplatesController {
* Migrates page content to templates if needed.
*/
public function maybe_migrate_content() {
if ( ! $this->has_migrated_page( 'cart' ) ) {
$this->migrate_page( 'cart', CartTemplate::get_placeholder_page() );
}
if ( ! $this->has_migrated_page( 'checkout' ) ) {
$this->migrate_page( 'checkout', CheckoutTemplate::get_placeholder_page() );
}
}
/**
* Check if a page has been migrated to a template.
*
* @param string $page_id Page ID.
* @return boolean
*/
protected function has_migrated_page( $page_id ) {
return (bool) get_option( 'has_migrated_' . $page_id, false );
}
/**
* Prepare default page template.
*
* @param \WP_Post $page Page object.
* @return string
*/
protected function get_default_migrate_page_template( $page ) {
$default_template_content = $this->get_block_template_part( 'header' );
$default_template_content .= '
<!-- wp:group {"layout":{"inherit":true}} -->
<div class="wp-block-group">
<!-- wp:heading {"level":1} -->
<h1 class="wp-block-heading">' . wp_kses_post( $page->post_title ) . '</h1>
<!-- /wp:heading -->
' . wp_kses_post( $page->post_content ) . '
</div>
<!-- /wp:group -->
';
$default_template_content .= $this->get_block_template_part( 'footer' );
return $default_template_content;
}
/**
* Migrates a page to a template if needed.
*
* @param string $page_id Page ID.
* @param \WP_Post $page Page object.
*/
protected function migrate_page( $page_id, $page ) {
if ( ! $page || empty( $page->post_content ) ) {
update_option( 'has_migrated_' . $page_id, '1' );
// Migration should occur on a normal request to ensure every requirement is met.
// We are postponing it if WP is in maintenance mode, installing, WC installing or if the request is part of a WP-CLI command.
if ( wp_is_maintenance_mode() || ! get_option( 'woocommerce_db_version', false ) || Constants::is_defined( 'WP_SETUP_CONFIG' ) || Constants::is_defined( 'WC_INSTALLING' ) || Constants::is_defined( 'WP_CLI' ) ) {
return;
}
// Use the page template if it exists, which we'll use over our default template if found.
$existing_page_template = BlockTemplateUtils::get_block_template( get_stylesheet() . '//page', 'wp_template' );
if ( $existing_page_template && ! empty( $existing_page_template->content ) && strstr( $existing_page_template->content, 'wp:post-content' ) ) {
// Massage the original content into something we can use. Replace post content with a group block.
$pattern = '/(<!--\s*)wp:post-content(.*?)(\/-->)/';
$replacement = '
<!-- wp:group $2 -->
<div class="wp-block-group">' . wp_kses_post( $page->post_content ) . '</div>
<!-- /wp:group -->
';
$template_content = preg_replace( $pattern, $replacement, $existing_page_template->content );
} else {
$template_content = $this->get_default_migrate_page_template( $page );
if ( ! BlockTemplateMigrationUtils::has_migrated_page( 'cart' ) ) {
BlockTemplateMigrationUtils::migrate_page( 'cart' );
}
$new_page_template = BlockTemplateUtils::get_block_template( 'woocommerce/woocommerce//' . $page_id, 'wp_template' );
// Check template validity--template must exist, and custom template must not be present already.
if ( ! $new_page_template || $new_page_template->wp_id ) {
update_option( 'has_migrated_' . $page_id, '1' );
return;
if ( ! BlockTemplateMigrationUtils::has_migrated_page( 'checkout' ) ) {
BlockTemplateMigrationUtils::migrate_page( 'checkout' );
}
$new_page_template_id = wp_insert_post(
[
'post_name' => $new_page_template->slug,
'post_type' => 'wp_template',
'post_status' => 'publish',
'tax_input' => array(
'wp_theme' => $new_page_template->theme,
),
'meta_input' => array(
'origin' => $new_page_template->source,
),
'post_content' => $template_content,
],
true
);
if ( ! is_wp_error( $new_page_template_id ) ) {
update_option( 'has_migrated_' . $page_id, '1' );
}
}
/**
* Returns the requested template part.
*
* @param string $part The part to return.
*
* @return string
*/
protected function get_block_template_part( $part ) {
$template_part = BlockTemplateUtils::get_block_template( get_stylesheet() . '//' . $part, 'wp_template_part' );
if ( ! $template_part || empty( $template_part->content ) ) {
return '';
}
return $template_part->content;
}
/**
* Replaces page settings in WooCommerce with text based permalinks which point to a template.
*
* @param array $settings Settings pages.
* @return array
*/
public function template_permalink_settings( $settings ) {
foreach ( $settings as $key => $setting ) {
if ( 'woocommerce_checkout_page_id' === $setting['id'] ) {
$checkout_page = CheckoutTemplate::get_placeholder_page();
$settings[ $key ] = [
'title' => __( 'Checkout page', 'woocommerce' ),
'desc' => sprintf(
// translators: %1$s: opening anchor tag, %2$s: closing anchor tag.
__( 'The checkout template can be %1$s edited here%2$s.', 'woocommerce' ),
'<a href="' . esc_url( admin_url( 'site-editor.php?postType=wp_template&postId=woocommerce%2Fwoocommerce%2F%2F' . CheckoutTemplate::get_slug() ) ) . '" target="_blank">',
'</a>'
),
'desc_tip' => __( 'This is the URL to the checkout page.', 'woocommerce' ),
'id' => 'woocommerce_checkout_page_endpoint',
'type' => 'permalink',
'default' => $checkout_page ? $checkout_page->post_name : CheckoutTemplate::get_slug(),
'autoload' => false,
];
}
if ( 'woocommerce_cart_page_id' === $setting['id'] ) {
$cart_page = CartTemplate::get_placeholder_page();
$settings[ $key ] = [
'title' => __( 'Cart page', 'woocommerce' ),
'desc' => sprintf(
// translators: %1$s: opening anchor tag, %2$s: closing anchor tag.
__( 'The cart template can be %1$s edited here%2$s.', 'woocommerce' ),
'<a href="' . esc_url( admin_url( 'site-editor.php?postType=wp_template&postId=woocommerce%2Fwoocommerce%2F%2F' . CartTemplate::get_slug() ) ) . '" target="_blank">',
'</a>'
),
'desc_tip' => __( 'This is the URL to the cart page.', 'woocommerce' ),
'id' => 'woocommerce_cart_page_endpoint',
'type' => 'permalink',
'default' => $cart_page ? $cart_page->post_name : CartTemplate::get_slug(),
'autoload' => false,
];
}
}
return $settings;
}
/**
* Syncs entered permalink with the pages and returns the correct value.
*
* @param string $value Value of the option.
* @param string $option Name of the option.
* @return string
*/
public function update_template_permalink( $value, $option ) {
if ( 'woocommerce_checkout_page_endpoint' === $option ) {
return $this->sync_endpoint_with_page( CheckoutTemplate::get_placeholder_page(), 'checkout', $value );
}
if ( 'woocommerce_cart_page_endpoint' === $option ) {
return $this->sync_endpoint_with_page( CartTemplate::get_placeholder_page(), 'cart', $value );
}
return $value;
}
/**
* Syncs the provided permalink with the actual WP page.
*
* @param WP_Post|null $page The page object, or null if it does not exist.
* @param string $page_slug The identifier for the page e.g. cart, checkout.
* @param string $permalink The new permalink to use.
* @return string THe actual permalink assigned to the page. May differ from $permalink if it was already taken.
*/
protected function sync_endpoint_with_page( $page, $page_slug, $permalink ) {
if ( ! $page ) {
$updated_page_id = wc_create_page(
esc_sql( $permalink ),
'woocommerce_' . $page_slug . '_page_id',
$page_slug,
'',
'',
'publish'
);
} else {
$updated_page_id = wp_update_post(
[
'ID' => $page->ID,
'post_name' => esc_sql( $permalink ),
]
);
}
// Get post again in case slug was updated with a suffix.
if ( $updated_page_id && ! is_wp_error( $updated_page_id ) ) {
return get_post( $updated_page_id )->post_name;
}
return $permalink;
}
}

View File

@@ -83,7 +83,7 @@ abstract class AbstractBlock {
$render_callback_attributes = $this->parse_render_callback_attributes( $attributes );
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
$this->enqueue_assets( $render_callback_attributes );
$this->enqueue_assets( $render_callback_attributes, $content, $block );
}
return $this->render( $render_callback_attributes, $content, $block );
}
@@ -210,6 +210,37 @@ abstract class AbstractBlock {
}
$metadata_path = $this->asset_api->get_block_metadata_path( $this->block_name );
/**
* We always want to load block styles separately, for every theme.
* When the core assets are loaded separately, other blocks' styles get
* enqueued separately too. Thus we only need to handle the remaining
* case.
*/
if (
! is_admin() &&
! wc_current_theme_is_fse_theme() &&
$block_settings['style'] &&
(
! function_exists( 'wp_should_load_separate_core_block_assets' ) ||
! wp_should_load_separate_core_block_assets()
)
) {
$style_handles = $block_settings['style'];
$block_settings['style'] = null;
add_filter(
'render_block',
function( $html, $block ) use ( $style_handles ) {
if ( $block['blockName'] === $this->get_block_type() ) {
array_map( 'wp_enqueue_style', $style_handles );
}
return $html;
},
10,
2
);
}
// Prefer to register with metadata if the path is set in the block's class.
if ( ! empty( $metadata_path ) ) {
register_block_type_from_metadata(
@@ -364,9 +395,11 @@ abstract class AbstractBlock {
* @internal This prevents the block script being enqueued on all pages. It is only enqueued as needed. Note that
* we intentionally do not pass 'script' to register_block_type.
*
* @param array $attributes Any attributes that currently are available from the block.
* @param array $attributes Any attributes that currently are available from the block.
* @param string $content The block content.
* @param WP_Block $block The block object.
*/
protected function enqueue_assets( array $attributes ) {
protected function enqueue_assets( array $attributes, $content, $block ) {
if ( $this->enqueued_assets ) {
return;
}

View File

@@ -513,10 +513,10 @@ abstract class AbstractProductGrid extends AbstractDynamicBlock {
'woocommerce_blocks_product_grid_item_html',
"<li class=\"wc-block-grid__product\">
<a href=\"{$data->permalink}\" class=\"wc-block-grid__product-link\">
{$data->badge}
{$data->image}
{$data->title}
</a>
{$data->badge}
{$data->price}
{$data->rating}
{$data->button}
@@ -649,6 +649,7 @@ abstract class AbstractProductGrid extends AbstractDynamicBlock {
'data-quantity' => '1',
'data-product_id' => $product->get_id(),
'data-product_sku' => $product->get_sku(),
'data-price' => wc_get_price_to_display( $product ),
'rel' => 'nofollow',
'class' => 'wp-block-button__link ' . ( function_exists( 'wc_wp_theme_get_element_class_name' ) ? wc_wp_theme_get_element_class_name( 'button' ) : '' ) . ' add_to_cart_button',
);
@@ -678,13 +679,12 @@ abstract class AbstractProductGrid extends AbstractDynamicBlock {
*/
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'min_columns', wc_get_theme_support( 'product_blocks::min_columns', 1 ), true );
$this->asset_data_registry->add( 'max_columns', wc_get_theme_support( 'product_blocks::max_columns', 6 ), true );
$this->asset_data_registry->add( 'default_columns', wc_get_theme_support( 'product_blocks::default_columns', 3 ), true );
$this->asset_data_registry->add( 'min_rows', wc_get_theme_support( 'product_blocks::min_rows', 1 ), true );
$this->asset_data_registry->add( 'max_rows', wc_get_theme_support( 'product_blocks::max_rows', 6 ), true );
$this->asset_data_registry->add( 'default_rows', wc_get_theme_support( 'product_blocks::default_rows', 3 ), true );
$this->asset_data_registry->add( 'stock_status_options', wc_get_product_stock_status_options(), true );
$this->asset_data_registry->add( 'minColumns', wc_get_theme_support( 'product_blocks::min_columns', 1 ), true );
$this->asset_data_registry->add( 'maxColumns', wc_get_theme_support( 'product_blocks::max_columns', 6 ), true );
$this->asset_data_registry->add( 'defaultColumns', wc_get_theme_support( 'product_blocks::default_columns', 3 ), true );
$this->asset_data_registry->add( 'minRows', wc_get_theme_support( 'product_blocks::min_rows', 1 ), true );
$this->asset_data_registry->add( 'maxRows', wc_get_theme_support( 'product_blocks::max_rows', 6 ), true );
$this->asset_data_registry->add( 'defaultRows', wc_get_theme_support( 'product_blocks::default_rows', 3 ), true );
}
/**

View File

@@ -96,7 +96,7 @@ class AddToCartForm extends AbstractBlock {
$classname = $attributes['className'] ?? '';
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$product_classname = $is_descendent_of_single_product_block ? 'product' : '';
$product_classname = $is_descendent_of_single_product_block ? 'product' : '';
$form = sprintf(
'<div class="wp-block-add-to-cart-form %1$s %2$s %3$s" style="%4$s">%5$s</div>',
@@ -162,6 +162,11 @@ class AddToCartForm extends AbstractBlock {
public function add_to_cart_redirect_filter( $url ) {
// phpcs:ignore
if ( isset( $_POST['is-descendent-of-single-product-block'] ) && 'true' == $_POST['is-descendent-of-single-product-block'] ) {
if ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) {
return wc_get_cart_url();
}
return wp_validate_redirect( wp_get_referer(), $url );
}

View File

@@ -22,29 +22,20 @@ class AllProducts extends AbstractBlock {
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
// Set this so filter blocks being used as widgets know when to render.
$this->asset_data_registry->add( 'has_filterable_products', true, true );
$this->asset_data_registry->add( 'hasFilterableProducts', true, true );
$this->asset_data_registry->add( 'minColumns', wc_get_theme_support( 'product_blocks::min_columns', 1 ), true );
$this->asset_data_registry->add( 'maxColumns', wc_get_theme_support( 'product_blocks::max_columns', 6 ), true );
$this->asset_data_registry->add( 'defaultColumns', wc_get_theme_support( 'product_blocks::default_columns', 3 ), true );
$this->asset_data_registry->add( 'minRows', wc_get_theme_support( 'product_blocks::min_rows', 1 ), true );
$this->asset_data_registry->add( 'maxRows', wc_get_theme_support( 'product_blocks::max_rows', 6 ), true );
$this->asset_data_registry->add( 'defaultRows', wc_get_theme_support( 'product_blocks::default_rows', 3 ), true );
$this->asset_data_registry->add( 'min_columns', wc_get_theme_support( 'product_blocks::min_columns', 1 ), true );
$this->asset_data_registry->add( 'max_columns', wc_get_theme_support( 'product_blocks::max_columns', 6 ), true );
$this->asset_data_registry->add( 'default_columns', wc_get_theme_support( 'product_blocks::default_columns', 3 ), true );
$this->asset_data_registry->add( 'min_rows', wc_get_theme_support( 'product_blocks::min_rows', 1 ), true );
$this->asset_data_registry->add( 'max_rows', wc_get_theme_support( 'product_blocks::max_rows', 6 ), true );
$this->asset_data_registry->add( 'default_rows', wc_get_theme_support( 'product_blocks::default_rows', 3 ), true );
// Hydrate the following data depending on admin or frontend context.
// Hydrate the All Product block with data from the API. This is for the add to cart buttons which show current quantity in cart, and events.
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
$this->hydrate_from_api();
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
}
}
/**
* Hydrate the All Product block with data from the API. This is for the add to cart buttons which show current
* quantity in cart, and events.
*/
protected function hydrate_from_api() {
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
}
/**
* It is necessary to register and enqueue assets during the render phase because we want to load assets only if the block has the content.
*/

View File

@@ -34,12 +34,32 @@ class Cart extends AbstractBlock {
add_action( 'wp_loaded', array( $this, 'register_patterns' ) );
}
/**
* Dequeues the scripts added by WC Core to the Cart page.
*
* @return void
*/
public function dequeue_woocommerce_core_scripts() {
wp_dequeue_script( 'wc-cart' );
wp_dequeue_script( 'wc-password-strength-meter' );
wp_dequeue_script( 'selectWoo' );
wp_dequeue_style( 'select2' );
}
/**
* Register block pattern for Empty Cart Message to make it translatable.
*/
public function register_patterns() {
$shop_permalink = wc_get_page_id( 'shop' ) ? get_permalink( wc_get_page_id( 'shop' ) ) : '';
register_block_pattern(
'woocommerce/cart-heading',
array(
'title' => '',
'inserter' => false,
'content' => '<!-- wp:heading {"align":"wide", "level":1} --><h1 class="wp-block-heading alignwide">' . esc_html__( 'Cart', 'woocommerce' ) . '</h1><!-- /wp:heading -->',
)
);
register_block_pattern(
'woocommerce/cart-cross-sells-message',
array(
@@ -112,16 +132,18 @@ class Cart extends AbstractBlock {
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
* @param array $attributes Any attributes that currently are available from the block.
* @param array $attributes Any attributes that currently are available from the block.
* @param string $content The block content.
* @param WP_Block $block The block object.
*/
protected function enqueue_assets( array $attributes ) {
protected function enqueue_assets( array $attributes, $content, $block ) {
/**
* Fires before cart block scripts are enqueued.
*
* @since 2.6.0
*/
do_action( 'woocommerce_blocks_enqueue_cart_block_scripts_before' );
parent::enqueue_assets( $attributes );
parent::enqueue_assets( $attributes, $content, $block );
/**
* Fires after cart block scripts are enqueued.
*
@@ -139,11 +161,8 @@ class Cart extends AbstractBlock {
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
// Deregister core cart scripts and styles.
wp_dequeue_script( 'wc-cart' );
wp_dequeue_script( 'wc-password-strength-meter' );
wp_dequeue_script( 'selectWoo' );
wp_dequeue_style( 'select2' );
// Dequeue the core scripts when rendering this block.
add_action( 'wp_enqueue_scripts', array( $this, 'dequeue_woocommerce_core_scripts' ), 20 );
/**
* We need to check if $content has any templates from prior iterations of the block, in order to update to the latest iteration.
@@ -231,7 +250,7 @@ class Cart extends AbstractBlock {
// Hydrate the following data depending on admin or frontend context.
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
$this->hydrate_from_api();
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
}
/**
@@ -242,19 +261,6 @@ class Cart extends AbstractBlock {
do_action( 'woocommerce_blocks_cart_enqueue_data' );
}
/**
* Hydrate the cart block with data from the API.
*/
protected function hydrate_from_api() {
// Cache existing notices now, otherwise they are caught by the Cart Controller and converted to exceptions.
$old_notices = WC()->session->get( 'wc_notices', array() );
wc_clear_notices();
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
// Restore notices.
WC()->session->set( 'wc_notices', $old_notices );
}
/**
* Register script and style assets for the block type before it is registered.
*

View File

@@ -24,6 +24,51 @@ class Checkout extends AbstractBlock {
*/
protected $chunks_folder = 'checkout-blocks';
/**
* Initialize this block type.
*
* - Hook into WP lifecycle.
* - Register the block with WordPress.
*/
protected function initialize() {
parent::initialize();
add_action( 'wp_loaded', array( $this, 'register_patterns' ) );
// This prevents the page redirecting when the cart is empty. This is so the editor still loads the page preview.
add_filter(
'woocommerce_checkout_redirect_empty_cart',
function( $return ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return isset( $_GET['_wp-find-template'] ) ? false : $return;
}
);
}
/**
* Dequeues the scripts added by WC Core to the Checkout page.
*
* @return void
*/
public function dequeue_woocommerce_core_scripts() {
wp_dequeue_script( 'wc-checkout' );
wp_dequeue_script( 'wc-password-strength-meter' );
wp_dequeue_script( 'selectWoo' );
wp_dequeue_style( 'select2' );
}
/**
* Register block pattern for Empty Cart Message to make it translatable.
*/
public function register_patterns() {
register_block_pattern(
'woocommerce/checkout-heading',
array(
'title' => '',
'inserter' => false,
'content' => '<!-- wp:heading {"align":"wide", "level":1} --><h1 class="wp-block-heading alignwide">' . esc_html__( 'Checkout', 'woocommerce' ) . '</h1><!-- /wp:heading -->',
)
);
}
/**
* Get the editor script handle for this block type.
*
@@ -67,16 +112,18 @@ class Checkout extends AbstractBlock {
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
* @param array $attributes Any attributes that currently are available from the block.
* @param array $attributes Any attributes that currently are available from the block.
* @param string $content The block content.
* @param WP_Block $block The block object.
*/
protected function enqueue_assets( array $attributes ) {
protected function enqueue_assets( array $attributes, $content, $block ) {
/**
* Fires before checkout block scripts are enqueued.
*
* @since 4.6.0
*/
do_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_before' );
parent::enqueue_assets( $attributes );
parent::enqueue_assets( $attributes, $content, $block );
/**
* Fires after checkout block scripts are enqueued.
*
@@ -94,17 +141,15 @@ class Checkout extends AbstractBlock {
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( $this->is_checkout_endpoint() ) {
// Note: Currently the block only takes care of the main checkout form -- if an endpoint is set, refer to the
// legacy shortcode instead and do not render block.
return wc_current_theme_is_fse_theme() ? do_shortcode( '[woocommerce_checkout]' ) : '[woocommerce_checkout]';
}
// Deregister core checkout scripts and styles.
wp_dequeue_script( 'wc-checkout' );
wp_dequeue_script( 'wc-password-strength-meter' );
wp_dequeue_script( 'selectWoo' );
wp_dequeue_style( 'select2' );
// Dequeue the core scripts when rendering this block.
add_action( 'wp_enqueue_scripts', array( $this, 'dequeue_woocommerce_core_scripts' ), 20 );
/**
* We need to check if $content has any templates from prior iterations of the block, in order to update to the latest iteration.
@@ -297,8 +342,34 @@ class Checkout extends AbstractBlock {
$this->asset_data_registry->add( 'globalPaymentMethods', $formatted_payment_methods );
}
if ( $is_block_editor && ! $this->asset_data_registry->exists( 'incompatibleExtensions' ) ) {
if ( ! class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) {
return;
}
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$declared_extensions = \Automattic\WooCommerce\Utilities\FeaturesUtil::get_compatible_plugins_for_feature( 'cart_checkout_blocks' );
$incompatible_extensions = array_reduce(
$declared_extensions['incompatible'],
function( $acc, $item ) {
$plugin = get_plugin_data( WP_PLUGIN_DIR . '/' . $item );
$acc[] = [
'id' => $plugin['TextDomain'],
'title' => $plugin['Name'],
];
return $acc;
},
[]
);
$this->asset_data_registry->add( 'incompatibleExtensions', $incompatible_extensions );
}
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
$this->hydrate_from_api();
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
$this->asset_data_registry->hydrate_data_from_api_request( 'checkoutData', '/wc/store/v1/checkout' );
$this->hydrate_customer_payment_methods();
}
@@ -366,25 +437,6 @@ class Checkout extends AbstractBlock {
remove_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 );
}
/**
* Hydrate the checkout block with data from the API.
*/
protected function hydrate_from_api() {
// Cache existing notices now, otherwise they are caught by the Cart Controller and converted to exceptions.
$old_notices = WC()->session->get( 'wc_notices', array() );
wc_clear_notices();
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
add_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );
$rest_preload_api_requests = rest_preload_api_request( [], '/wc/store/v1/checkout' );
$this->asset_data_registry->add( 'checkoutData', $rest_preload_api_requests['/wc/store/v1/checkout']['body'] ?? [] );
remove_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );
// Restore notices.
WC()->session->set( 'wc_notices', $old_notices );
}
/**
* Callback for woocommerce_payment_methods_list_item filter to add token id
* to the generated list.

View File

@@ -0,0 +1,127 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use WC_Shortcode_Cart;
use WC_Shortcode_Checkout;
use WC_Frontend_Scripts;
/**
* Classic Shortcode class
*
* @internal
*/
class ClassicShortcode extends AbstractDynamicBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'classic-shortcode';
/**
* API version.
*
* @var string
*/
protected $api_version = '2';
/**
* Render method for the Classic Template block. This method will determine which template to render.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string | void Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( ! isset( $attributes['shortcode'] ) ) {
return;
}
/**
* We need to load the scripts here because when using block templates wp_head() gets run after the block
* template. As a result we are trying to enqueue required scripts before we have even registered them.
*
* @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/5328#issuecomment-989013447
*/
if ( class_exists( 'WC_Frontend_Scripts' ) ) {
$frontend_scripts = new WC_Frontend_Scripts();
$frontend_scripts::load_scripts();
}
if ( 'cart' === $attributes['shortcode'] ) {
return $this->render_cart( $attributes );
}
if ( 'checkout' === $attributes['shortcode'] ) {
return $this->render_checkout( $attributes );
}
return "You're using the ClassicShortcode block";
}
/**
* Get the list of classes to apply to this block.
*
* @param array $attributes Block attributes. Default empty array.
* @return string space-separated list of classes.
*/
protected function get_container_classes( $attributes = array() ) {
$classes = array( 'wp-block-group' );
if ( isset( $attributes['align'] ) ) {
$classes[] = "align{$attributes['align']}";
}
return implode( ' ', $classes );
}
/**
* Render method for rendering the cart shortcode.
*
* @param array $attributes Block attributes.
* @return string Rendered block type output.
*/
protected function render_cart( $attributes ) {
if ( ! isset( WC()->cart ) ) {
return '';
}
ob_start();
echo '<div class="' . esc_attr( $this->get_container_classes( $attributes ) ) . '">';
WC_Shortcode_Cart::output( array() );
echo '</div>';
return ob_get_clean();
}
/**
* Render method for rendering the checkout shortcode.
*
* @param array $attributes Block attributes.
* @return string Rendered block type output.
*/
protected function render_checkout( $attributes ) {
if ( ! isset( WC()->cart ) ) {
return '';
}
ob_start();
echo '<div class="' . esc_attr( $this->get_container_classes( $attributes ) ) . '">';
WC_Shortcode_Checkout::output( array() );
echo '</div>';
return ob_get_clean();
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -6,9 +6,10 @@ use Automattic\WooCommerce\Blocks\Templates\ProductSearchResultsTemplate;
use Automattic\WooCommerce\Blocks\Templates\OrderConfirmationTemplate;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
use WC_Shortcode_Checkout;
use WC_Frontend_Scripts;
/**
* Classic Single Product class
* Classic Template class
*
* @internal
*/
@@ -47,10 +48,17 @@ class ClassicTemplate extends AbstractDynamicBlock {
public function enqueue_block_assets() {
// Ensures frontend styles for blocks exist in the site editor iframe.
if ( class_exists( 'WC_Frontend_Scripts' ) && is_admin() ) {
$frontend_scripts = new \WC_Frontend_Scripts();
$frontend_scripts = new WC_Frontend_Scripts();
$styles = $frontend_scripts::get_styles();
foreach ( $styles as $handle => $style ) {
wp_enqueue_style( $handle, $style['src'], $style['deps'], $style['version'], $style['media'] );
wp_enqueue_style(
$handle,
set_url_scheme( $style['src'] ),
$style['deps'],
$style['version'],
$style['media']
);
}
}
}
@@ -75,7 +83,7 @@ class ClassicTemplate extends AbstractDynamicBlock {
* @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/5328#issuecomment-989013447
*/
if ( class_exists( 'WC_Frontend_Scripts' ) ) {
$frontend_scripts = new \WC_Frontend_Scripts();
$frontend_scripts = new WC_Frontend_Scripts();
$frontend_scripts::load_scripts();
}
@@ -83,7 +91,7 @@ class ClassicTemplate extends AbstractDynamicBlock {
return $this->render_order_received();
}
if ( 'single-product' === $attributes['template'] ) {
if ( is_product() ) {
return $this->render_single_product();
}
@@ -97,13 +105,13 @@ class ClassicTemplate extends AbstractDynamicBlock {
if ( in_array( $attributes['template'], $archive_templates, true ) ) {
// Set this so that our product filters can detect if it's a PHP template.
$this->asset_data_registry->add( 'is_rendering_php_template', true, true );
$this->asset_data_registry->add( 'isRenderingPhpTemplate', true, true );
// Set this so filter blocks being used as widgets know when to render.
$this->asset_data_registry->add( 'has_filterable_products', true, true );
$this->asset_data_registry->add( 'hasFilterableProducts', true, true );
$this->asset_data_registry->add(
'page_url',
'pageUrl',
html_entity_decode( get_pagenum_link() ),
''
);
@@ -165,9 +173,16 @@ class ClassicTemplate extends AbstractDynamicBlock {
*/
do_action( 'woocommerce_before_main_content' );
while ( have_posts() ) :
$product_query = new \WP_Query(
array(
'post_type' => 'product',
'p' => get_the_ID(),
)
);
the_post();
while ( $product_query->have_posts() ) :
$product_query->the_post();
wc_get_template_part( 'content', 'single-product' );
endwhile;

View File

@@ -0,0 +1,292 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
/**
* CollectionFilters class.
*/
final class CollectionFilters extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'collection-filters';
/**
* Mapping inner blocks to CollectionData API parameters.
*
* @var array
*/
protected $collection_data_params_mapping = array(
'calculate_price_range' => 'woocommerce/collection-price-filter',
'calculate_stock_status_counts' => 'woocommerce/collection-stock-filter',
'calculate_attribute_counts' => 'woocommerce/collection-attribute-filter',
'calculate_rating_counts' => 'woocommerce/collection-rating-filter',
);
/**
* Cache the current response from the API.
*
* @var array
*/
private $current_response = null;
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Get the frontend script handle for this block type.
*
* @param string $key Data to get, or default to everything.
*
* @return null This block has no frontend script.
*/
protected function get_block_type_script( $key = null ) {
return null;
}
/**
* Initialize this block type.
*
* - Hook into WP lifecycle.
* - Register the block with WordPress.
*/
protected function initialize() {
parent::initialize();
add_action( 'render_block_context', array( $this, 'modify_inner_blocks_context' ), 10, 3 );
}
/**
* Extra data passed through from server to client for block.
*
* @param array $attributes Any attributes that currently are available from the block.
* Note, this will be empty in the editor context when the block is
* not in the post content on editor load.
*/
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
if ( ! is_admin() ) {
/**
* At this point, WP starts rendering the Collection Filters block,
* we can safely unset the current response.
*/
$this->current_response = null;
}
}
/**
* Modify the context of inner blocks.
*
* @param array $context The block context.
* @param array $parsed_block The parsed block.
* @param WP_Block $parent_block The parent block.
* @return array
*/
public function modify_inner_blocks_context( $context, $parsed_block, $parent_block ) {
if ( is_admin() || ! is_a( $parent_block, 'WP_Block' ) ) {
return $context;
}
/**
* Bail if the current block is not a direct child of CollectionFilters
* and the parent block doesn't have our custom context.
*/
if (
"woocommerce/{$this->block_name}" !== $parent_block->name &&
empty( $parent_block->context['isCollectionFiltersInnerBlock'] )
) {
return $context;
}
/**
* The first time we reach here, WP is rendering the first direct child
* of CollectionFilters block. We hydrate and cache the collection data
* response for other inner blocks to use.
*/
if ( ! isset( $this->current_response ) ) {
$this->current_response = $this->get_aggregated_collection_data( $parent_block );
}
if ( empty( $this->current_response ) ) {
return $context;
}
/**
* We target only filter blocks, but they can be nested inside other
* blocks like Group/Row for layout purposes. We pass this custom light
* weight context (instead of full CollectionData response) to all inner
* blocks of current CollectionFilters to find and iterate inner filter
* blocks.
*/
$context['isCollectionFiltersInnerBlock'] = true;
if (
isset( $parsed_block['blockName'] ) &&
in_array( $parsed_block['blockName'], $this->collection_data_params_mapping, true )
) {
$context['collectionData'] = $this->current_response;
}
return $context;
}
/**
* Get the aggregated collection data from the API.
* Loop through inner blocks and build a query string to pass to the API.
*
* @param WP_Block $block The block instance.
* @return array
*/
private function get_aggregated_collection_data( $block ) {
$inner_blocks = $this->get_inner_blocks_recursive( $block->inner_blocks );
$collection_data_params = array_map(
function( $block_name ) use ( $inner_blocks ) {
return in_array( $block_name, $inner_blocks, true );
},
$this->collection_data_params_mapping
);
if ( empty( array_filter( $collection_data_params ) ) ) {
return array();
}
$products_params = $this->get_formatted_products_params( $block->context['query'] );
$response = Package::container()->get( Hydration::class )->get_rest_api_response_data(
add_query_arg(
array_merge(
$products_params,
$collection_data_params,
),
'/wc/store/v1/products/collection-data'
)
);
if ( ! empty( $response['body'] ) ) {
return $response['body'];
}
return array();
}
/**
* Get all inner blocks recursively.
*
* @param WP_Block_List $inner_blocks The block to get inner blocks from.
* @param array $results The results array.
*
* @return array
*/
private function get_inner_blocks_recursive( $inner_blocks, &$results = array() ) {
if ( is_a( $inner_blocks, 'WP_Block_List' ) ) {
foreach ( $inner_blocks as $inner_block ) {
$results[] = $inner_block->name;
$this->get_inner_blocks_recursive(
$inner_block->inner_blocks,
$results
);
}
}
return $results;
}
/**
* Get formatted products params for ProductCollectionData route from the
* query context.
*
* @param array $query The query context.
* @return array
*/
private function get_formatted_products_params( $query ) {
$params = array();
if ( empty( $query['isProductCollectionBlock'] ) ) {
return $params;
}
/**
* The following params can be passed directly to Store API endpoints.
*/
$shared_params = array( 'exclude', 'offset', 'order', 'serach' );
array_walk(
$shared_params,
function( $key ) use ( $query, &$params ) {
$params[ $key ] = $query[ $key ] ?? '';
}
);
/**
* The following params just need to transform the key, their value can
* be passed as it is to the Store API.
*/
$mapped_params = array(
'orderBy' => 'orderby',
'pages' => 'page',
'perPage' => 'per_page',
'woocommerceStockStatus' => 'stock_status',
'woocommerceOnSale' => 'on_sale',
'woocommerceHandPickedProducts' => 'include',
);
array_walk(
$mapped_params,
function( $mapped_key, $original_key ) use ( $query, &$params ) {
$params[ $mapped_key ] = $query[ $original_key ] ?? '';
}
);
/**
* The value of taxQuery and woocommerceAttributes need additional
* transformation to the shape that Store API accepts.
*/
$taxonomy_mapper = function( $key ) {
$mapping = array(
'product_tag' => 'tag',
'product_cat' => 'category',
);
return $mapping[ $key ] ?? '_unstable_tax_' . $key;
};
if ( is_array( $query['taxQuery'] ) ) {
array_walk(
$query['taxQuery'],
function( $terms, $taxonomy ) use ( $taxonomy_mapper, &$params ) {
$params[ $taxonomy_mapper( $taxonomy ) ] = implode( ',', $terms );
}
);
}
if ( is_array( $query['woocommerceAttributes'] ) ) {
array_walk(
$query['woocommerceAttributes'],
function( $attribute ) use ( &$params ) {
$params['attributes'][] = array(
'attribute' => $attribute['taxonomy'],
'term_id' => $attribute['termId'],
);
}
);
}
/**
* Product Collection determines the product visibility based on stock
* statuses. We need to pass the catalog_visibility param to the Store
* API to make sure the product visibility is correct.
*/
$params['catalog_visibility'] = is_search() ? 'catalog' : 'visible';
return array_filter( $params );
}
}

View File

@@ -0,0 +1,183 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use stdClass;
/**
* CollectionPriceFilter class.
*/
final class CollectionPriceFilter extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'collection-price-filter';
const MIN_PRICE_QUERY_VAR = 'min_price';
const MAX_PRICE_QUERY_VAR = 'max_price';
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
// Short circuit if the collection data isn't ready yet.
if ( empty( $block->context['collectionData']['price_range'] ) ) {
return $content;
}
$price_range = $block->context['collectionData']['price_range'];
$wrapper_attributes = get_block_wrapper_attributes();
$min_range = $price_range->min_price / 10 ** $price_range->currency_minor_unit;
$max_range = $price_range->max_price / 10 ** $price_range->currency_minor_unit;
$min_price = intval( get_query_var( self::MIN_PRICE_QUERY_VAR, $min_range ) );
$max_price = intval( get_query_var( self::MAX_PRICE_QUERY_VAR, $max_range ) );
$data = array(
'minPrice' => $min_price,
'maxPrice' => $max_price,
'minRange' => $min_range,
'maxRange' => $max_range,
);
wc_store(
array(
'state' => array(
'filters' => $data,
),
)
);
$filter_reset_button = sprintf(
' <button class="wc-block-components-filter-reset-button" data-wc-on--click="actions.filters.reset">
<span aria-hidden="true">%1$s</span>
<span class="screen-reader-text">%2$s</span>
</button>',
__( 'Reset', 'woocommerce' ),
__( 'Reset filter', 'woocommerce' ),
);
return sprintf(
'<div %1$s>
<div class="controls">%2$s</div>
<div class="actions">
%3$s
</div>
</div>',
$wrapper_attributes,
$this->get_price_slider( $data, $attributes ),
$filter_reset_button
);
}
/**
* Get the price slider HTML.
*
* @param array $store_data The data passing to Interactivity Store.
* @param array $attributes Block attributes.
*/
private function get_price_slider( $store_data, $attributes ) {
list (
'showInputFields' => $show_input_fields,
'inlineInput' => $inline_input
) = $attributes;
list (
'minPrice' => $min_price,
'maxPrice' => $max_price,
'minRange' => $min_range,
'maxRange' => $max_range,
) = $store_data;
// CSS variables for the range bar style.
$__low = 100 * $min_price / $max_range;
$__high = 100 * $max_price / $max_range;
$range_style = "--low: $__low%; --high: $__high%";
$formatted_min_price = wc_price( $min_price, array( 'decimals' => 0 ) );
$formatted_max_price = wc_price( $max_price, array( 'decimals' => 0 ) );
$classes = $show_input_fields && $inline_input ? 'price-slider inline-input' : 'price-slider';
$price_min = $show_input_fields ?
sprintf(
'<input
class="min"
type="text"
value="%d"
data-wc-bind--value="state.filters.minPrice"
data-wc-on--input="actions.filters.setMinPrice"
data-wc-on--change="actions.filters.updateProducts"
/>',
esc_attr( $min_price )
) : sprintf(
'<span data-wc-text="state.filters.formattedMinPrice">%s</span>',
esc_attr( $formatted_min_price )
);
$price_max = $show_input_fields ?
sprintf(
'<input
class="max"
type="text"
value="%d"
data-wc-bind--value="state.filters.maxPrice"
data-wc-on--input="actions.filters.setMaxPrice"
data-wc-on--change="actions.filters.updateProducts"
/>',
esc_attr( $max_price )
) : sprintf(
'<span data-wc-text="state.filters.formattedMaxPrice">%s</span>',
esc_attr( $formatted_max_price )
);
ob_start();
?>
<div class="<?php echo esc_attr( $classes ); ?>">
<div
class="range"
style="<?php echo esc_attr( $range_style ); ?>"
data-wc-bind--style="state.filters.rangeStyle"
>
<div class="range-bar"></div>
<input
type="range"
class="min"
min="<?php echo esc_attr( $min_range ); ?>"
max="<?php echo esc_attr( $max_range ); ?>"
value="<?php echo esc_attr( $min_price ); ?>"
data-wc-bind--max="state.filters.maxRange"
data-wc-bind--value="state.filters.minPrice"
data-wc-class--active="state.filters.isMinActive"
data-wc-on--input="actions.filters.setMinPrice"
data-wc-on--change="actions.filters.updateProducts"
>
<input
type="range"
class="max"
min="<?php echo esc_attr( $min_range ); ?>"
max="<?php echo esc_attr( $max_range ); ?>"
value="<?php echo esc_attr( $max_price ); ?>"
data-wc-bind--max="state.filters.maxRange"
data-wc-bind--value="state.filters.maxPrice"
data-wc-class--active="state.filters.isMaxActive"
data-wc-on--input="actions.filters.setMaxPrice"
data-wc-on--change="actions.filters.updateProducts"
>
</div>
<div class="text">
<?php // $price_min and $price_max are escapsed in the sprintf() calls above. ?>
<?php echo $price_min; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php echo $price_max; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div>
</div>
<?php
return ob_get_clean();
}
}

View File

@@ -152,7 +152,7 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
$classes[] = ' has-parallax';
}
return sprintf( '<div class="%1$s" style="%2$s" /></div>', implode( ' ', $classes ), $styles );
return sprintf( '<div class="%1$s" style="%2$s" /></div>', esc_attr( implode( ' ', $classes ) ), esc_attr( $styles ) );
}
/**
@@ -201,7 +201,8 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
* @return string
*/
private function render_image( $attributes, $item, string $image_url ) {
$style = sprintf( 'object-fit: %s;', $attributes['imageFit'] );
$style = sprintf( 'object-fit: %s;', esc_attr( $attributes['imageFit'] ) );
$img_alt = $attributes['alt'] ?: $this->get_item_title( $item );
if ( $this->hasFocalPoint( $attributes ) ) {
$style .= sprintf(
@@ -214,10 +215,10 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
if ( ! empty( $image_url ) ) {
return sprintf(
'<img alt="%1$s" class="wc-block-%2$s__background-image" src="%3$s" style="%4$s" />',
wp_kses_post( $attributes['alt'] ?: $this->get_item_title( $item ) ),
esc_attr( $img_alt ),
$this->block_name,
$image_url,
$style
esc_url( $image_url ),
esc_attr( $style )
);
}
@@ -321,7 +322,6 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
*/
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'min_height', wc_get_theme_support( 'featured_block::min_height', 500 ), true );
$this->asset_data_registry->add( 'default_height', wc_get_theme_support( 'featured_block::default_height', 500 ), true );
$this->asset_data_registry->add( 'defaultHeight', wc_get_theme_support( 'featured_block::default_height', 500 ), true );
}
}

View File

@@ -21,7 +21,7 @@ class HandpickedProducts extends AbstractProductGrid {
$ids = array_map( 'absint', $this->attributes['products'] );
$query_args['post__in'] = $ids;
$query_args['posts_per_page'] = count( $ids );
$query_args['posts_per_page'] = count( $ids ); // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page
}
/**

View File

@@ -9,6 +9,7 @@ use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Blocks\Utils\Utils;
use Automattic\WooCommerce\Blocks\Utils\MiniCartUtils;
/**
* Mini-Cart class.
@@ -156,7 +157,7 @@ class MiniCart extends AbstractBlock {
if (
current_user_can( 'edit_theme_options' ) &&
wc_current_theme_is_fse_theme()
( wc_current_theme_is_fse_theme() || current_theme_supports( 'block-template-parts' ) )
) {
$theme_slug = BlockTemplateUtils::theme_has_template_part( 'mini-cart' ) ? wp_get_theme()->get_stylesheet() : BlockTemplateUtils::PLUGIN_SLUG;
@@ -351,9 +352,9 @@ class MiniCart extends AbstractBlock {
if ( isset( $attributes['hasHiddenPrice'] ) && false !== $attributes['hasHiddenPrice'] ) {
return;
}
$price_color = array_key_exists( 'priceColorValue', $attributes ) ? $attributes['priceColorValue'] : '';
$price_color = array_key_exists( 'priceColor', $attributes ) ? $attributes['priceColor']['color'] : '';
return '<span class="wc-block-mini-cart__amount" style="color:' . $price_color . ' "></span>' . $this->get_include_tax_label_markup( $attributes );
return '<span class="wc-block-mini-cart__amount" style="color:' . esc_attr( $price_color ) . ' "></span>' . $this->get_include_tax_label_markup( $attributes );
}
/**
@@ -367,9 +368,9 @@ class MiniCart extends AbstractBlock {
if ( empty( $this->tax_label ) ) {
return '';
}
$price_color = array_key_exists( 'priceColorValue', $attributes ) ? $attributes['priceColorValue'] : '';
$price_color = array_key_exists( 'priceColor', $attributes ) ? $attributes['priceColor']['color'] : '';
return '<small class="wc-block-mini-cart__tax-label" style="color:' . $price_color . ' " hidden>' . esc_html( $this->tax_label ) . '</small>';
return '<small class="wc-block-mini-cart__tax-label" style="color:' . esc_attr( $price_color ) . ' " hidden>' . esc_html( $this->tax_label ) . '</small>';
}
/**
@@ -381,7 +382,7 @@ class MiniCart extends AbstractBlock {
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
return $content . $this->get_markup( $attributes );
return $content . $this->get_markup( MiniCartUtils::migrate_attributes_to_color_panel( $attributes ) );
}
/**
@@ -405,8 +406,8 @@ class MiniCart extends AbstractBlock {
}
$wrapper_styles = $classes_styles['styles'];
$icon_color = array_key_exists( 'iconColorValue', $attributes ) ? $attributes['iconColorValue'] : 'currentColor';
$product_count_color = array_key_exists( 'productCountColorValue', $attributes ) ? $attributes['productCountColorValue'] : '';
$icon_color = array_key_exists( 'iconColor', $attributes ) ? esc_attr( $attributes['iconColor']['color'] ) : 'currentColor';
$product_count_color = array_key_exists( 'productCountColor', $attributes ) ? esc_attr( $attributes['productCountColor']['color'] ) : '';
// Default "Cart" icon.
$icon = '<svg class="wc-block-mini-cart__icon" width="32" height="32" viewBox="0 0 32 32" fill="' . $icon_color . '" xmlns="http://www.w3.org/2000/svg">
@@ -443,7 +444,7 @@ class MiniCart extends AbstractBlock {
}
// It is not necessary to load the Mini-Cart Block on Cart and Checkout page.
return '<div class="' . $wrapper_classes . '" style="visibility:hidden" aria-hidden="true">
return '<div class="' . esc_attr( $wrapper_classes ) . '" style="visibility:hidden" aria-hidden="true">
<button class="wc-block-mini-cart__button" disabled>' . $button_html . '</button>
</div>';
}

View File

@@ -75,10 +75,12 @@ class MiniCartContents extends AbstractBlock {
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
* @param array $attributes Any attributes that currently are available from the block.
* @param array $attributes Any attributes that currently are available from the block.
* @param string $content The block content.
* @param WP_Block $block The block object.
*/
protected function enqueue_assets( array $attributes ) {
parent::enqueue_assets( $attributes );
protected function enqueue_assets( array $attributes, $content, $block ) {
parent::enqueue_assets( $attributes, $content, $block );
$text_color = StyleAttributesUtils::get_text_color_class_and_style( $attributes );
$bg_color = StyleAttributesUtils::get_background_color_class_and_style( $attributes );

View File

@@ -0,0 +1,301 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* AbstractOrderConfirmationBlock class.
*/
abstract class AbstractOrderConfirmationBlock extends AbstractBlock {
/**
* Initialize this block type.
*
* - Hook into WP lifecycle.
* - Register the block with WordPress.
*/
protected function initialize() {
parent::initialize();
add_action( 'wp_loaded', array( $this, 'register_patterns' ) );
}
/**
* Get the content from a hook and return it.
*
* @param string $hook Hook name.
* @param array $args Array of args to pass to the hook.
* @return string
*/
protected function get_hook_content( $hook, $args ) {
ob_start();
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
do_action_ref_array( $hook, $args );
return ob_get_clean();
}
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
*
* @return string | void Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
$order = $this->get_order();
$permission = $this->get_view_order_permissions( $order );
$block_content = $order ? $this->render_content( $order, $permission, $attributes, $content ) : $this->render_content_fallback();
$classname = $attributes['className'] ?? '';
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
if ( ! empty( $classes_and_styles['classes'] ) ) {
$classname .= ' ' . $classes_and_styles['classes'];
}
return $block_content ? sprintf(
'<div class="wc-block-%4$s %1$s" style="%2$s">%3$s</div>',
esc_attr( trim( $classname ) ),
esc_attr( $classes_and_styles['styles'] ),
$block_content,
esc_attr( $this->block_name )
) : '';
}
/**
* This renders the content of the block within the wrapper. The permission determines what data can be shown under
* the given context.
*
* @param \WC_Order $order Order object.
* @param string $permission Permission level for viewing order details.
* @param array $attributes Block attributes.
* @param string $content Original block content.
* @return string
*/
abstract protected function render_content( $order, $permission = false, $attributes = [], $content = '' );
/**
* This is what gets rendered when the order does not exist. Renders nothing by default, but can be overridden by
* child classes.
*
* @return string
*/
protected function render_content_fallback() {
return '';
}
/**
* Get current order.
*
* @return \WC_Order|null
*/
protected function get_order() {
$order_id = absint( get_query_var( 'order-received' ) );
if ( $order_id ) {
return wc_get_order( $order_id );
}
return null;
}
/**
* View mode for order details based on the order, current user, and settings.
*
* Possible values are:
* - "full" user can view all order details.
* - "limited" user can view some order details, but no PII. This may happen for example, if the user checked out as a guest.
* - false user cannot view order details.
*
* @param \WC_Order|null $order Order object.
* @return "full"|"limited"|false
*/
protected function get_view_order_permissions( $order ) {
if ( ! $order || ! $this->has_valid_order_key( $order ) ) {
return false; // Always disallow access to invalid orders and those without a valid key.
}
// For customers with accounts, verify the order belongs to the current user or disallow access.
if ( $this->is_customer_order( $order ) ) {
return $this->is_current_customer_order( $order ) ? 'full' : false;
}
// Guest orders are displayed with limited information.
return $this->email_verification_required( $order ) ? false : 'limited';
}
/**
* See if guest checkout is enabled.
*
* @return boolean
*/
protected function allow_guest_checkout() {
return 'yes' === get_option( 'woocommerce_enable_guest_checkout' );
}
/**
* Guest users without an active session can provide their email address to view order details. This however can only
* be permitted if the user also provided the correct order key, and guest checkout is actually enabled.
*
* @param \WC_Order $order Order object.
* @return boolean
*/
protected function email_verification_permitted( $order ) {
return $this->allow_guest_checkout() && $this->has_valid_order_key( $order ) && ! $this->is_customer_order( $order );
}
/**
* See if we need to verify the email address before showing the order details.
*
* @param \WC_Order $order Order object.
* @return boolean
*/
protected function email_verification_required( $order ) {
// Skip verification if the current user still has the order in their session.
if ( $order->get_id() === wc()->session->get( 'store_api_draft_order' ) ) {
return false;
}
/**
* Controls the grace period within which we do not require any sort of email verification step before rendering
* the 'order received' or 'order pay' pages.
*
* @see \WC_Shortcode_Checkout::order_received()
* @since 11.4.0
* @param int $grace_period Time in seconds after an order is placed before email verification may be required.
* @param \WC_Order $order The order for which this grace period is being assessed.
* @param string $context Indicates the context in which we might verify the email address. Typically 'order-pay' or 'order-received'.
*/
$verification_grace_period = (int) apply_filters( 'woocommerce_order_email_verification_grace_period', 10 * MINUTE_IN_SECONDS, $order, 'order-received' );
$date_created = $order->get_date_created();
// We do not need to verify the email address if we are within the grace period immediately following order creation.
if ( is_a( $date_created, \WC_DateTime::class ) && time() - $date_created->getTimestamp() <= $verification_grace_period ) {
return false;
}
$session = wc()->session;
$session_email = '';
$session_order = 0;
if ( is_a( $session, \WC_Session::class ) ) {
$customer = $session->get( 'customer' );
$session_email = is_array( $customer ) && isset( $customer['email'] ) ? sanitize_email( $customer['email'] ) : '';
$session_order = (int) $session->get( 'store_api_draft_order' );
}
// We do not need to verify the email address if the user still has the order in session.
if ( $order->get_id() === $session_order ) {
return false;
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
if ( ! empty( $_POST ) && ! wp_verify_nonce( $_POST['check_submission'] ?? '', 'wc_verify_email' ) ) {
return true;
}
$session_email_match = $session_email === $order->get_billing_email();
$supplied_email_match = isset( $_POST['email'] ) && sanitize_email( wp_unslash( $_POST['email'] ) ?? '' ) === $order->get_billing_email();
// If we cannot match the order with the current user, the user should verify their email address.
$email_verification_required = ! $session_email_match && ! $supplied_email_match;
/**
* Provides an opportunity to override the (potential) requirement for shoppers to verify their email address
* before we show information such as the order summary, or order payment page.
*
* @see \WC_Shortcode_Checkout::order_received()
* @since 11.4.0
* @param bool $email_verification_required If email verification is required.
* @param WC_Order $order The relevant order.
* @param string $context The context under which we are performing this check.
*/
return (bool) apply_filters( 'woocommerce_order_email_verification_required', $email_verification_required, $order, 'order-received' );
}
/**
* See if the order key is valid.
*
* @param \WC_Order $order Order object.
* @return boolean
*/
protected function has_valid_order_key( $order ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return ! empty( $_GET['key'] ) && $order->key_is_valid( wc_clean( wp_unslash( $_GET['key'] ) ) );
}
/**
* See if the current order came from a guest or a logged in customer.
*
* @param \WC_Order $order Order object.
* @return boolean
*/
protected function is_customer_order( $order ) {
return 0 < $order->get_user_id();
}
/**
* See if the current logged in user ID matches the given order customer ID.
*
* Returns false for logged-out customers.
*
* @param \WC_Order $order Order object.
* @return boolean
*/
protected function is_current_customer_order( $order ) {
return $this->is_customer_order( $order ) && $order->get_user_id() === get_current_user_id();
}
/**
* Get the frontend script handle for this block type.
*
* @param string $key Data to get, or default to everything.
*/
protected function get_block_type_script( $key = null ) {
return null;
}
/**
* Register block pattern for Order Confirmation to make it translatable.
*/
public function register_patterns() {
register_block_pattern(
'woocommerce/order-confirmation-totals-heading',
array(
'title' => '',
'inserter' => false,
'content' => '<!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"24px"}}} --><h3 class="wp-block-heading" style="font-size:24px">' . esc_html__( 'Order details', 'woocommerce' ) . '</h3><!-- /wp:heading -->',
)
);
register_block_pattern(
'woocommerce/order-confirmation-downloads-heading',
array(
'title' => '',
'inserter' => false,
'content' => '<!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"24px"}}} --><h3 class="wp-block-heading" style="font-size:24px">' . esc_html__( 'Downloads', 'woocommerce' ) . '</h3><!-- /wp:heading -->',
)
);
register_block_pattern(
'woocommerce/order-confirmation-shipping-heading',
array(
'title' => '',
'inserter' => false,
'content' => '<!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"24px"}}} --><h3 class="wp-block-heading" style="font-size:24px">' . esc_html__( 'Shipping address', 'woocommerce' ) . '</h3><!-- /wp:heading -->',
)
);
register_block_pattern(
'woocommerce/order-confirmation-billing-heading',
array(
'title' => '',
'inserter' => false,
'content' => '<!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"24px"}}} --><h3 class="wp-block-heading" style="font-size:24px">' . esc_html__( 'Billing address', 'woocommerce' ) . '</h3><!-- /wp:heading -->',
)
);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
/**
* AdditionalInformation class.
*/
class AdditionalInformation extends AbstractOrderConfirmationBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'order-confirmation-additional-information';
/**
* This renders the content of the block within the wrapper.
*
* @param \WC_Order $order Order object.
* @param string $permission Permission level for viewing order details.
* @param array $attributes Block attributes.
* @param string $content Original block content.
* @return string
*/
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
if ( ! $permission ) {
return $content;
}
$this->remove_core_hooks();
$content .= $this->get_hook_content( 'woocommerce_thankyou_' . $order->get_payment_method(), [ $order->get_id() ] );
$content .= $this->get_hook_content( 'woocommerce_thankyou', [ $order->get_id() ] );
$this->restore_core_hooks();
return $content;
}
/**
* Remove core hooks from the thankyou page.
*/
protected function remove_core_hooks() {
remove_action( 'woocommerce_thankyou', 'woocommerce_order_details_table', 10 );
}
/**
* Restore core hooks from the thankyou page.
*/
protected function restore_core_hooks() {
add_action( 'woocommerce_thankyou', 'woocommerce_order_details_table', 10 );
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
/**
* BillingAddress class.
*/
class BillingAddress extends AbstractOrderConfirmationBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'order-confirmation-billing-address';
/**
* This renders the content of the block within the wrapper.
*
* @param \WC_Order $order Order object.
* @param string $permission Permission level for viewing order details.
* @param array $attributes Block attributes.
* @param string $content Original block content.
* @return string
*/
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
if ( 'full' !== $permission || ! $order->has_billing_address() ) {
return '';
}
$address = '<address>' . wp_kses_post( $order->get_formatted_billing_address() ) . '</address>';
$phone = $order->get_billing_phone() ? '<p class="woocommerce-customer-details--phone">' . esc_html( $order->get_billing_phone() ) . '</p>' : '';
return $address . $phone;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
/**
* BillingWrapper class.
*/
class BillingWrapper extends AbstractOrderConfirmationBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'order-confirmation-billing-wrapper';
/**
* This renders the content of the billing wrapper.
*
* @param \WC_Order $order Order object.
* @param string $permission Permission level for viewing order details.
* @param array $attributes Block attributes.
* @param string $content Original block content.
*/
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
if ( ! $order || ! $order->has_billing_address() || 'full' !== $permission ) {
return '';
}
return $content;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* Downloads class.
*/
class Downloads extends AbstractOrderConfirmationBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'order-confirmation-downloads';
/**
* This renders the content of the block within the wrapper.
*
* @param \WC_Order $order Order object.
* @param string $permission Permission level for viewing order details.
* @param array $attributes Block attributes.
* @param string $content Original block content.
* @return string
*/
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
$show_downloads = $order && $order->has_downloadable_item() && $order->is_download_permitted();
$downloads = $order ? $order->get_downloadable_items() : [];
if ( ! $permission || ! $show_downloads ) {
return $this->render_content_fallback();
}
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, [ 'border_color', 'border_radius', 'border_width', 'border_style', 'background_color', 'text_color' ] );
return '
<table cellspacing="0" class="wc-block-order-confirmation-downloads__table ' . esc_attr( $classes_and_styles['classes'] ) . '" style="' . esc_attr( $classes_and_styles['styles'] ) . '">
<thead>
<tr>
' . $this->render_order_downloads_column_headers( $order ) . '
</td>
</thead>
<tbody>
' . $this->render_order_downloads( $order, $downloads ) . '
</tbody>
</table>
';
}
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
* @param array $attributes Any attributes that currently are available from the block.
* @return string
*/
protected function get_inline_styles( array $attributes ) {
$link_classes_and_styles = StyleAttributesUtils::get_link_color_class_and_style( $attributes );
$link_hover_classes_and_styles = StyleAttributesUtils::get_link_hover_color_class_and_style( $attributes );
$border_classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, [ 'border_color', 'border_radius', 'border_width', 'border_style' ] );
return '
.wc-block-order-confirmation-downloads__table a {' . $link_classes_and_styles['style'] . '}
.wc-block-order-confirmation-downloads__table a:hover, .wc-block-order-confirmation-downloads__table a:focus {' . $link_hover_classes_and_styles['style'] . '}
.wc-block-order-confirmation-downloads__table {' . $border_classes_and_styles['styles'] . '}
.wc-block-order-confirmation-downloads__table th, .wc-block-order-confirmation-downloads__table td {' . $border_classes_and_styles['styles'] . '}
';
}
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
* @param array $attributes Any attributes that currently are available from the block.
* @param string $content The block content.
* @param \WP_Block $block The block object.
*/
protected function enqueue_assets( array $attributes, $content, $block ) {
parent::enqueue_assets( $attributes, $content, $block );
$styles = $this->get_inline_styles( $attributes );
wp_add_inline_style( 'wc-blocks-style', $styles );
}
/**
* Render column headers for downloads table.
*
* @return string
*/
protected function render_order_downloads_column_headers() {
$columns = wc_get_account_downloads_columns();
$return = '';
foreach ( $columns as $column_id => $column_name ) {
$return .= '<th class="' . esc_attr( $column_id ) . '"><span class="nobr">' . esc_html( $column_name ) . '</span></th>';
}
return $return;
}
/**
* Render downloads.
*
* @param \WC_Order $order Order object.
* @param array $downloads Array of downloads.
* @return string
*/
protected function render_order_downloads( $order, $downloads ) {
$return = '';
foreach ( $downloads as $download ) {
$return .= '<tr>' . $this->render_order_download_row( $download ) . '</tr>';
}
return $return;
}
/**
* Render a download row in the table.
*
* @param array $download Download data.
* @return string
*/
protected function render_order_download_row( $download ) {
$return = '';
foreach ( wc_get_account_downloads_columns() as $column_id => $column_name ) {
$return .= '<td class="' . esc_attr( $column_id ) . '" data-title="' . esc_attr( $column_name ) . '">';
if ( has_action( 'woocommerce_account_downloads_column_' . $column_id ) ) {
$return .= $this->get_hook_content( 'woocommerce_account_downloads_column_' . $column_id, [ $download ] );
} else {
switch ( $column_id ) {
case 'download-product':
if ( $download['product_url'] ) {
$return .= '<a href="' . esc_url( $download['product_url'] ) . '">' . esc_html( $download['product_name'] ) . '</a>';
} else {
$return .= esc_html( $download['product_name'] );
}
break;
case 'download-file':
$return .= '<a href="' . esc_url( $download['download_url'] ) . '" class="woocommerce-MyAccount-downloads-file button alt">' . esc_html( $download['download_name'] ) . '</a>';
break;
case 'download-remaining':
$return .= is_numeric( $download['downloads_remaining'] ) ? esc_html( $download['downloads_remaining'] ) : esc_html__( '&infin;', 'woocommerce' );
break;
case 'download-expires':
if ( ! empty( $download['access_expires'] ) ) {
$return .= '<time datetime="' . esc_attr( gmdate( 'Y-m-d', strtotime( $download['access_expires'] ) ) ) . '" title="' . esc_attr( strtotime( $download['access_expires'] ) ) . '">' . esc_html( date_i18n( get_option( 'date_format' ), strtotime( $download['access_expires'] ) ) ) . '</time>';
} else {
$return .= esc_html__( 'Never', 'woocommerce' );
}
break;
}
}
$return .= '</td>';
}
return $return;
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
/**
* DownloadsWrapper class.
*/
class DownloadsWrapper extends AbstractOrderConfirmationBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'order-confirmation-downloads-wrapper';
/**
* See if the store has a downloadable product. This controls if we bother to show a preview in the editor.
*
* @return boolean
*/
protected function store_has_downloadable_products() {
$has_downloadable_product = get_transient( 'wc_blocks_has_downloadable_product', false );
if ( false === $has_downloadable_product ) {
$product_ids = get_posts(
array(
'post_type' => 'product',
'numberposts' => 1,
'post_status' => 'publish',
'fields' => 'ids',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
array(
'key' => '_downloadable',
'value' => 'yes',
'compare' => '=',
),
),
)
);
$has_downloadable_product = ! empty( $product_ids );
set_transient( 'wc_blocks_has_downloadable_product', $has_downloadable_product ? '1' : '0', MONTH_IN_SECONDS );
}
return (bool) $has_downloadable_product;
}
/**
* Extra data passed through from server to client for block.
*
* @param array $attributes Any attributes that currently are available from the block.
* Note, this will be empty in the editor context when the block is
* not in the post content on editor load.
*/
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'storeHasDownloadableProducts', $this->store_has_downloadable_products() );
}
/**
* This renders the content of the downloads wrapper.
*
* @param \WC_Order $order Order object.
* @param string $permission Permission level for viewing order details.
* @param array $attributes Block attributes.
* @param string $content Original block content.
*/
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
$show_downloads = $order && $order->has_downloadable_item() && $order->is_download_permitted();
if ( ! $show_downloads || ! $permission ) {
return '';
}
return $content;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
/**
* ShippingAddress class.
*/
class ShippingAddress extends AbstractOrderConfirmationBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'order-confirmation-shipping-address';
/**
* This renders the content of the block within the wrapper.
*
* @param \WC_Order $order Order object.
* @param string $permission Permission level for viewing order details.
* @param array $attributes Block attributes.
* @param string $content Original block content.
* @return string
*/
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
if ( ! $permission || ! $order->needs_shipping_address() || ! $order->has_shipping_address() ) {
return $this->render_content_fallback();
}
if ( 'full' === $permission ) {
$address = '<address>' . wp_kses_post( $order->get_formatted_shipping_address() ) . '</address>';
$phone = $order->get_shipping_phone() ? '<p class="woocommerce-customer-details--phone">' . esc_html( $order->get_shipping_phone() ) . '</p>' : '';
return $address . $phone;
}
$states = wc()->countries->get_states( $order->get_shipping_country() );
$address = esc_html(
sprintf(
/* translators: %s location. */
__( 'Shipping to %s', 'woocommerce' ),
implode(
', ',
array_filter(
[
$order->get_shipping_postcode(),
$order->get_shipping_city(),
$states[ $order->get_shipping_state() ] ?? $order->get_shipping_state(),
wc()->countries->countries[ $order->get_shipping_country() ] ?? $order->get_shipping_country(),
]
)
)
)
);
return '<address>' . wp_kses_post( $address ) . '</address>';
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
/**
* ShippingWrapper class.
*/
class ShippingWrapper extends AbstractOrderConfirmationBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'order-confirmation-shipping-wrapper';
/**
* This renders the content of the shipping wrapper.
*
* @param \WC_Order $order Order object.
* @param string $permission Permission level for viewing order details.
* @param array $attributes Block attributes.
* @param string $content Original block content.
*/
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
if ( ! $order || ! $order->has_shipping_address() || ! $order->needs_shipping_address() || ! $permission ) {
return '';
}
return $content;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -0,0 +1,236 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* Status class.
*/
class Status extends AbstractOrderConfirmationBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'order-confirmation-status';
/**
* This block uses a custom render method so that the email verification form can be appended to the block. This does
* not inherit styles from the parent block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
*
* @return string | void Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
$order = $this->get_order();
$classname = $attributes['className'] ?? '';
if ( isset( $attributes['align'] ) ) {
$classname .= " align{$attributes['align']}";
}
$block = parent::render( $attributes, $content, $block );
if ( ! $block ) {
return '';
}
$additional_content = $this->render_confirmation_notice( $order );
return $additional_content ? $block . sprintf(
'<div class="wc-block-order-confirmation-status-description %1$s">%2$s</div>',
esc_attr( trim( $classname ) ),
$additional_content
) : $block;
}
/**
* This renders the content of the block within the wrapper.
*
* @param \WC_Order $order Order object.
* @param string $permission Permission level for viewing order details.
* @param array $attributes Block attributes.
* @param string $content Original block content.
* @return string
*/
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
if ( ! $permission ) {
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
return '<p>' . wp_kses_post( apply_filters( 'woocommerce_thankyou_order_received_text', esc_html__( 'Thank you. Your order has been received.', 'woocommerce' ), null ) ) . '</p>';
}
$content = $this->get_hook_content( 'woocommerce_before_thankyou', [ $order ] );
$status = $order->get_status();
// Unlike the core handling, this includes some extra messaging for completed orders so the text is appropriate for other order statuses.
switch ( $status ) {
case 'cancelled':
$content .= '<p>' . wp_kses_post(
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
apply_filters(
'woocommerce_thankyou_order_received_text',
esc_html__( 'Your order has been cancelled.', 'woocommerce' ),
$order
)
) . '</p>';
break;
case 'refunded':
$content .= '<p>' . wp_kses_post(
sprintf(
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
apply_filters(
'woocommerce_thankyou_order_received_text',
// translators: %s: date and time of the order refund.
esc_html__( 'Your order was refunded %s.', 'woocommerce' ),
$order
),
wc_format_datetime( $order->get_date_modified() )
)
) . '</p>';
break;
case 'completed':
$content .= '<p>' . wp_kses_post(
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
apply_filters(
'woocommerce_thankyou_order_received_text',
esc_html__( 'Thank you. Your order has been fulfilled.', 'woocommerce' ),
$order
)
) . '</p>';
break;
case 'failed':
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$order_received_text = apply_filters( 'woocommerce_thankyou_order_received_text', esc_html__( 'Your order cannot be processed as the originating bank/merchant has declined your transaction. Please attempt your purchase again.', 'woocommerce' ), null );
$actions = '';
if ( 'full' === $permission ) {
$actions .= '<a href="' . esc_url( $order->get_checkout_payment_url() ) . '" class="button">' . esc_html__( 'Try again', 'woocommerce' ) . '</a> ';
}
if ( wc_get_page_permalink( 'myaccount' ) ) {
$actions .= '<a href="' . esc_url( wc_get_page_permalink( 'myaccount' ) ) . '" class="button">' . esc_html__( 'My account', 'woocommerce' ) . '</a> ';
}
$content .= '
<p>' . $order_received_text . '</p>
<p class="wc-block-order-confirmation-status__actions">' . $actions . '</p>
';
break;
default:
$content .= '<p>' . wp_kses_post(
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
apply_filters(
'woocommerce_thankyou_order_received_text',
esc_html__( 'Thank you. Your order has been received.', 'woocommerce' ),
$order
)
) . '</p>';
break;
}
return $content;
}
/**
* This is what gets rendered when the order does not exist.
*
* @return string
*/
protected function render_content_fallback() {
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
return '<p>' . esc_html__( 'Please check your email for the order confirmation.', 'woocommerce' ) . '</p>';
}
/**
* If the order is invalid or there is no permission to view the details, tell the user to check email or log-in.
*
* @param \WC_Order|null $order Order object.
* @return string
*/
protected function render_confirmation_notice( $order = null ) {
if ( ! $order ) {
$content = '<p>' . esc_html__( 'If you\'ve just placed an order, give your email a quick check for the confirmation.', 'woocommerce' );
if ( wc_get_page_permalink( 'myaccount' ) ) {
$content .= ' ' . sprintf(
/* translators: 1: opening a link tag 2: closing a link tag */
esc_html__( 'Have an account with us? %1$sLog in here to view your order details%2$s.', 'woocommerce' ),
'<a href="' . esc_url( wc_get_page_permalink( 'myaccount' ) ) . '" class="button">',
'</a>'
);
}
$content .= '</p>';
return $content;
}
$permission = $this->get_view_order_permissions( $order );
if ( $permission ) {
return '';
}
$verification_required = $this->email_verification_required( $order );
$verification_permitted = $this->email_verification_permitted( $order );
$my_account_page = wc_get_page_permalink( 'myaccount' );
$content = '<p>';
$content .= esc_html__( 'Great news! Your order has been received, and a confirmation will be sent to your email address.', 'woocommerce' );
$content .= $my_account_page ? ' ' . sprintf(
/* translators: 1: opening a link tag 2: closing a link tag */
esc_html__( 'Have an account with us? %1$sLog in here%2$s to view your order.', 'woocommerce' ),
'<a href="' . esc_url( $my_account_page ) . '" class="button">',
'</a>'
) : '';
if ( $verification_required && $verification_permitted ) {
$content .= ' ' . esc_html__( 'Alternatively, confirm the email address linked to the order below.', 'woocommerce' );
}
$content .= '</p>';
if ( $verification_required && $verification_permitted ) {
$content .= $this->render_verification_form();
}
return $content;
}
/**
* Email verification for guest users.
*
* @return string
*/
protected function render_verification_form() {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$check_submission_notice = ! empty( $_POST ) ? wc_print_notice( esc_html__( 'We were unable to verify the email address you provided. Please try again.', 'woocommerce' ), 'error', [], true ) : '';
return '<form method="post" class="woocommerce-form woocommerce-verify-email">' .
$check_submission_notice .
sprintf(
'<p class="form-row verify-email">
<label for="%1$s">%2$s</label>
<input type="email" name="email" id="%1$s" autocomplete="email" class="input-text" required />
</p>',
esc_attr( 'verify-email' ),
esc_html__( 'Email address', 'woocommerce' ) . '&nbsp;<span class="required">*</span>'
) .
sprintf(
'<p class="form-row login-submit">
<input type="submit" name="wp-submit" id="%1$s" class="button button-primary %4$s" value="%2$s" />
%3$s
</p>',
esc_attr( 'verify-email-submit' ),
esc_html__( 'Confirm email and view order', 'woocommerce' ),
wp_nonce_field( 'wc_verify_email', 'check_submission', true, false ),
esc_attr( wc_wp_theme_get_element_class_name( 'button' ) )
) .
'</form>';
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
/**
* Summary class.
*/
class Summary extends AbstractOrderConfirmationBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'order-confirmation-summary';
/**
* This renders the content of the block within the wrapper.
*
* @param \WC_Order $order Order object.
* @param string $permission Permission level for viewing order details.
* @param array $attributes Block attributes.
* @param string $content Original block content.
* @return string
*/
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
if ( ! $permission ) {
return '';
}
$content = '<ul class="wc-block-order-confirmation-summary-list">';
$content .= $this->render_summary_row( __( 'Order number:', 'woocommerce' ), $order->get_order_number() );
$content .= $this->render_summary_row( __( 'Date:', 'woocommerce' ), wc_format_datetime( $order->get_date_created() ) );
$content .= $this->render_summary_row( __( 'Total:', 'woocommerce' ), $order->get_formatted_order_total() );
if ( 'full' === $permission ) {
$content .= $this->render_summary_row( __( 'Email:', 'woocommerce' ), $order->get_billing_email() );
$content .= $this->render_summary_row( __( 'Payment method:', 'woocommerce' ), $order->get_payment_method_title() );
}
$content .= '</ul>';
return $content;
}
/**
* Render row in the order summary.
*
* @param string $name name of row.
* @param string $value value of row.
* @return string
*/
protected function render_summary_row( $name, $value ) {
return $value ? '<li class="wc-block-order-confirmation-summary-list-item"><span class="wc-block-order-confirmation-summary-list-item__key">' . esc_html( $name ) . '</span> <span class="wc-block-order-confirmation-summary-list-item__value">' . wp_kses_post( $value ) . '</span></li>' : '';
}
}

View File

@@ -0,0 +1,229 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* Totals class.
*/
class Totals extends AbstractOrderConfirmationBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'order-confirmation-totals';
/**
* This renders the content of the block within the wrapper.
*
* @param \WC_Order $order Order object.
* @param string $permission Permission level for viewing order details.
* @param array $attributes Block attributes.
* @param string $content Original block content.
* @return string
*/
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
if ( ! $permission ) {
return $this->render_content_fallback();
}
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, [ 'border_color', 'border_radius', 'border_width', 'border_style', 'background_color', 'text_color' ] );
return $this->get_hook_content( 'woocommerce_order_details_before_order_table', [ $order ] ) . '
<table cellspacing="0" class="wc-block-order-confirmation-totals__table ' . esc_attr( $classes_and_styles['classes'] ) . '" style="' . esc_attr( $classes_and_styles['styles'] ) . '">
<thead>
<tr>
<th class="wc-block-order-confirmation-totals__product">' . esc_html__( 'Product', 'woocommerce' ) . '</th>
<th class="wc-block-order-confirmation-totals__total">' . esc_html__( 'Total', 'woocommerce' ) . '</th>
</tr>
</thead>
<tbody>
' . $this->get_hook_content( 'woocommerce_order_details_before_order_table_items', [ $order ] ) . '
' . $this->render_order_details_table_items( $order ) . '
' . $this->get_hook_content( 'woocommerce_order_details_after_order_table_items', [ $order ] ) . '
</tbody>
<tfoot>
' . $this->render_order_details_table_totals( $order ) . '
' . $this->render_order_details_table_customer_note( $order ) . '
</tfoot>
</table>
' . $this->get_hook_content( 'woocommerce_order_details_after_order_table', [ $order ] ) . '
' . $this->get_hook_content( 'woocommerce_after_order_details', [ $order ] ) . '
';
}
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
* @param array $attributes Any attributes that currently are available from the block.
* @return string
*/
protected function get_inline_styles( array $attributes ) {
$link_classes_and_styles = StyleAttributesUtils::get_link_color_class_and_style( $attributes );
$link_hover_classes_and_styles = StyleAttributesUtils::get_link_hover_color_class_and_style( $attributes );
$border_classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, [ 'border_color', 'border_radius', 'border_width', 'border_style' ] );
return '
.wc-block-order-confirmation-totals__table a {' . $link_classes_and_styles['style'] . '}
.wc-block-order-confirmation-totals__table a:hover, .wc-block-order-confirmation-totals__table a:focus {' . $link_hover_classes_and_styles['style'] . '}
.wc-block-order-confirmation-totals__table {' . $border_classes_and_styles['styles'] . '}
.wc-block-order-confirmation-totals__table th, .wc-block-order-confirmation-totals__table td {' . $border_classes_and_styles['styles'] . '}
';
}
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
* @param array $attributes Any attributes that currently are available from the block.
* @param string $content The block content.
* @param \WP_Block $block The block object.
*/
protected function enqueue_assets( array $attributes, $content, $block ) {
parent::enqueue_assets( $attributes, $content, $block );
$styles = $this->get_inline_styles( $attributes );
wp_add_inline_style( 'wc-blocks-style', $styles );
}
/**
* Render order details table items.
*
* Loosely based on the templates order-details.php and order-details-item.php from core.
*
* @param \WC_Order $order Order object.
* @return string
*/
protected function render_order_details_table_items( $order ) {
$return = '';
$order_items = array_filter(
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$order->get_items( apply_filters( 'woocommerce_purchase_order_item_types', 'line_item' ) ),
function( $item ) {
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
return apply_filters( 'woocommerce_order_item_visible', true, $item );
}
);
foreach ( $order_items as $item_id => $item ) {
$product = $item->get_product();
$return .= $this->render_order_details_table_item( $order, $item_id, $item, $product );
}
return $return;
}
/**
* Render an item in the order details table.
*
* @param \WC_Order $order Order object.
* @param integer $item_id Item ID.
* @param \WC_Order_Item $item Item object.
* @param \WC_Product|false $product Product object if it exists.
* @return string
*/
protected function render_order_details_table_item( $order, $item_id, $item, $product ) {
$is_visible = $product && $product->is_visible();
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$row_class = apply_filters( 'woocommerce_order_item_class', 'woocommerce-table__line-item order_item', $item, $order );
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$product_permalink = apply_filters( 'woocommerce_order_item_permalink', $is_visible ? $product->get_permalink( $item ) : '', $item, $order );
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$item_name = apply_filters(
'woocommerce_order_item_name',
$product_permalink ? sprintf( '<a href="%s">%s</a>', $product_permalink, $item->get_name() ) : $item->get_name(),
$item,
$is_visible
);
$qty = $item->get_quantity();
$refunded_qty = $order->get_qty_refunded_for_item( $item_id );
$qty_display = $refunded_qty ? '<del>' . esc_html( $qty ) . '</del> <ins>' . esc_html( $qty - ( $refunded_qty * -1 ) ) . '</ins>' : esc_html( $qty );
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$item_qty = apply_filters(
'woocommerce_order_item_quantity_html',
'<strong class="product-quantity">' . sprintf( '&times;&nbsp;%s', $qty_display ) . '</strong>',
$item
);
return '
<tr class="' . esc_attr( $row_class ) . '">
<td class="wc-block-order-confirmation-totals__product">
' . wp_kses_post( $item_name ) . '&nbsp;
' . wp_kses_post( $item_qty ) . '
' . $this->get_hook_content( 'woocommerce_order_item_meta_start', [ $item_id, $item, $order, false ] ) . '
' . wc_display_item_meta( $item, [ 'echo' => false ] ) . '
' . $this->get_hook_content( 'woocommerce_order_item_meta_end', [ $item_id, $item, $order, false ] ) . '
' . $this->render_order_details_table_item_purchase_note( $order, $product ) . '
</td>
<td class="wc-block-order-confirmation-totals__total">
' . wp_kses_post( $order->get_formatted_line_subtotal( $item ) ) . '
</td>
</tr>
';
}
/**
* Render an item purchase note.
*
* @param \WC_Order $order Order object.
* @param \WC_Product|false $product Product object if it exists.
* @return string
*/
protected function render_order_details_table_item_purchase_note( $order, $product ) {
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$show_purchase_note = $order->has_status( apply_filters( 'woocommerce_purchase_note_order_statuses', array( 'completed', 'processing' ) ) );
$purchase_note = $product ? $product->get_purchase_note() : '';
return $show_purchase_note && $purchase_note ? '<div class="product-purchase-note">' . wp_kses_post( $purchase_note ) . '</div>' : '';
}
/**
* Render order details table totals.
*
* @param \WC_Order $order Order object.
* @return string
*/
protected function render_order_details_table_totals( $order ) {
add_filter( 'woocommerce_order_shipping_to_display_shipped_via', '__return_empty_string' );
$return = '';
$total_rows = array_diff_key(
$order->get_order_item_totals(),
array(
'cart_subtotal' => '',
'payment_method' => '',
)
);
foreach ( $total_rows as $total ) {
$return .= '
<tr>
<th class="wc-block-order-confirmation-totals__label" scope="row">' . esc_html( $total['label'] ) . '</th>
<td class="wc-block-order-confirmation-totals__total">' . wp_kses_post( $total['value'] ) . '</td>
</tr>
';
}
return $return;
}
/**
* Render customer note.
*
* @param \WC_Order $order Order object.
* @return string
*/
protected function render_order_details_table_customer_note( $order ) {
if ( ! $order->get_customer_note() ) {
return '';
}
return '<tr>
<th class="wc-block-order-confirmation-totals__label" scope="row">' . esc_html__( 'Note:', 'woocommerce' ) . '</th>
<td class="wc-block-order-confirmation-totals__note">' . wp_kses_post( nl2br( wptexturize( $order->get_customer_note() ) ) ) . '</td>
</tr>';
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
/**
* TotalsWrapper class.
*/
class TotalsWrapper extends AbstractOrderConfirmationBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'order-confirmation-totals-wrapper';
/**
* This renders the content of the totals wrapper.
*
* @param \WC_Order $order Order object.
* @param string $permission Permission level for viewing order details.
* @param array $attributes Block attributes.
* @param string $content Original block content.
*/
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
if ( ! $permission ) {
return '';
}
return $content;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* Used in templates to wrap page content. Allows content to be populated at template level.
*
* @internal
*/
class PageContentWrapper extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'page-content-wrapper';
/**
* It isn't necessary to register block assets.
*
* @param string $key Data to get, or default to everything.
* @return array|string|null
*/
protected function get_block_type_script( $key = null ) {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* ProductAverageRating class.
*/
class ProductAverageRating extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-average-rating';
/**
* API version name.
*
* @var string
*/
protected $api_version = '2';
/**
* Get block supports. Shared with the frontend.
* IMPORTANT: If you change anything here, make sure to update the JS file too.
*
* @return array
*/
protected function get_block_type_supports() {
return array(
'color' =>
array(
'text' => true,
'background' => true,
'__experimentalSkipSerialization' => true,
),
'spacing' =>
array(
'margin' => true,
'padding' => true,
'__experimentalSkipSerialization' => true,
),
'typography' =>
array(
'fontSize' => true,
'__experimentalFontWeight' => true,
'__experimentalSkipSerialization' => true,
),
'__experimentalSelector' => '.wc-block-components-product-average-rating',
);
}
/**
* Overwrite parent method to prevent script registration.
*
* It is necessary to register and enqueues assets during the render
* phase because we want to load assets only if the block has the content.
*/
protected function register_block_type_assets() {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
$post_id = $block->context['postId'];
$product = wc_get_product( $post_id );
if ( ! $product || ! $product->get_review_count() ) {
return '';
}
$styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$text_align_styles_and_classes = StyleAttributesUtils::get_text_align_class_and_style( $attributes );
return sprintf(
'<div class="wc-block-components-product-average-rating-counter %1$s %2$s" style="%3$s">%4$s</div>',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
esc_attr( $styles_and_classes['classes'] ),
esc_attr( $styles_and_classes['styles'] ?? '' ),
$product->get_average_rating()
);
}
}

View File

@@ -15,11 +15,20 @@ class ProductButton extends AbstractBlock {
*/
protected $block_name = 'product-button';
/**
* It is necessary to register and enqueue assets during the render phase because we want to load assets only if the block has the content.
* Get the frontend script handle for this block type.
*
* @param string $key Data to get, or default to everything.
*/
protected function register_block_type_assets() {
return null;
protected function get_block_type_script( $key = null ) {
$script = [
'handle' => 'wc-' . $this->block_name . '-interactivity-frontend',
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-interactivity-frontend' ),
'dependencies' => [ 'wc-interactivity' ],
];
return $key ? $script[ $key ] : $script;
}
/**
@@ -29,6 +38,33 @@ class ProductButton extends AbstractBlock {
return [ 'query', 'queryId', 'postId' ];
}
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
* @param array $attributes Any attributes that currently are available from the block.
* @param string $content The block content.
* @param WP_Block $block The block object.
*/
protected function enqueue_assets( array $attributes, $content, $block ) {
parent::enqueue_assets( $attributes, $content, $block );
if ( wc_current_theme_is_fse_theme() ) {
add_action(
'wp_enqueue_scripts',
array( $this, 'dequeue_add_to_cart_scripts' )
);
} else {
$this->dequeue_add_to_cart_scripts();
}
}
/**
* Dequeue the add-to-cart script.
* The block uses Interactivity API, it isn't necessary enqueue the add-to-cart script.
*/
public function dequeue_add_to_cart_scripts() {
wp_dequeue_script( 'wc-add-to-cart' );
}
/**
* Include and render the block.
*
@@ -38,19 +74,26 @@ class ProductButton extends AbstractBlock {
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
$post_id = $block->context['postId'];
$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
$product = wc_get_product( $post_id );
if ( $product ) {
$cart_redirect_after_add = get_option( 'woocommerce_cart_redirect_after_add' ) === 'yes';
$ajax_add_to_cart_enabled = get_option( 'woocommerce_enable_ajax_add_to_cart' ) === 'yes';
$is_ajax_button = $ajax_add_to_cart_enabled && ! $cart_redirect_after_add && $product->supports( 'ajax_add_to_cart' ) && $product->is_purchasable() && $product->is_in_stock();
$html_element = $is_ajax_button ? 'button' : 'a';
$styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$text_align_styles_and_classes = StyleAttributesUtils::get_text_align_class_and_style( $attributes );
$classname = $attributes['className'] ?? '';
$custom_width_classes = isset( $attributes['width'] ) ? 'has-custom-width wp-block-button__width-' . $attributes['width'] : '';
$html_classes = implode(
$number_of_items_in_cart = $this->get_cart_item_quantities_by_product_id( $product->get_id() );
$more_than_one_item = $number_of_items_in_cart > 0;
$initial_product_text = $more_than_one_item ? sprintf(
/* translators: %s: product number. */
__( '%s in cart', 'woocommerce' ),
$number_of_items_in_cart
) : $product->add_to_cart_text();
$cart_redirect_after_add = get_option( 'woocommerce_cart_redirect_after_add' ) === 'yes';
$ajax_add_to_cart_enabled = get_option( 'woocommerce_enable_ajax_add_to_cart' ) === 'yes';
$is_ajax_button = $ajax_add_to_cart_enabled && ! $cart_redirect_after_add && $product->supports( 'ajax_add_to_cart' ) && $product->is_purchasable() && $product->is_in_stock();
$html_element = $is_ajax_button ? 'button' : 'a';
$styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$classname = $attributes['className'] ?? '';
$custom_width_classes = isset( $attributes['width'] ) ? 'has-custom-width wp-block-button__width-' . $attributes['width'] : '';
$custom_align_classes = isset( $attributes['textAlign'] ) ? 'align-' . $attributes['textAlign'] : '';
$html_classes = implode(
' ',
array_filter(
array(
@@ -60,10 +103,44 @@ class ProductButton extends AbstractBlock {
$product->is_purchasable() && $product->is_in_stock() ? 'add_to_cart_button' : '',
$is_ajax_button ? 'ajax_add_to_cart' : '',
'product_type_' . $product->get_type(),
$styles_and_classes['classes'],
esc_attr( $styles_and_classes['classes'] ),
)
)
);
wc_store(
array(
'state' => array(
'woocommerce' => array(
'inTheCartText' => sprintf(
/* translators: %s: product number. */
__( '%s in cart', 'woocommerce' ),
'###'
),
),
),
)
);
$default_quantity = 1;
$context = array(
'woocommerce' => array(
/**
* Filters the change the quantity to add to cart.
*
* @since 10.9.0
* @param number $default_quantity The default quantity.
* @param number $product_id The product id.
*/
'quantityToAdd' => apply_filters( 'woocommerce_add_to_cart_quantity', $default_quantity, $product->get_id() ),
'productId' => $product->get_id(),
'addToCartText' => null !== $product->add_to_cart_text() ? $product->add_to_cart_text() : __( 'Add to cart', 'woocommerce' ),
'temporaryNumberOfItems' => $number_of_items_in_cart,
'animationStatus' => 'IDLE',
),
);
/**
* Allow filtering of the add to cart button arguments.
*
@@ -87,6 +164,26 @@ class ProductButton extends AbstractBlock {
$args['attributes']['aria-label'] = wp_strip_all_tags( $args['attributes']['aria-label'] );
}
if ( isset( WC()->cart ) && ! WC()->cart->is_empty() ) {
$this->prevent_cache();
}
$div_directives = 'data-wc-context=\'' . wp_json_encode( $context, JSON_NUMERIC_CHECK ) . '\'';
$button_directives = '
data-wc-on--click="actions.woocommerce.addToCart"
data-wc-class--loading="context.woocommerce.isLoading"
';
$span_button_directives = '
data-wc-text="selectors.woocommerce.addToCartText"
data-wc-class--wc-block-slide-in="selectors.woocommerce.slideInAnimation"
data-wc-class--wc-block-slide-out="selectors.woocommerce.slideOutAnimation"
data-wc-layout-init="init.woocommerce.syncTemporaryNumberOfItemsOnLoad"
data-wc-effect="effects.woocommerce.startAnimation"
data-wc-on--animationend="actions.woocommerce.handleAnimationEnd"
';
/**
* Filters the add to cart button class.
*
@@ -96,22 +193,83 @@ class ProductButton extends AbstractBlock {
*/
return apply_filters(
'woocommerce_loop_add_to_cart_link',
sprintf(
'<div class="wp-block-button wc-block-components-product-button %1$s %2$s">
<%3$s href="%4$s" class="%5$s" style="%6$s" %7$s>%8$s</%3$s>
strtr(
'<div data-wc-interactive class="wp-block-button wc-block-components-product-button {classes} {custom_classes}"
{div_directives}
>
<{html_element}
href="{add_to_cart_url}"
class="{button_classes}"
style="{button_styles}"
{attributes}
{button_directives}
>
<span {span_button_directives}> {add_to_cart_text} </span>
</{html_element}>
{view_cart_html}
</div>',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
esc_attr( $classname . ' ' . $custom_width_classes ),
$html_element,
esc_url( $product->add_to_cart_url() ),
isset( $args['class'] ) ? esc_attr( $args['class'] ) : '',
esc_attr( $styles_and_classes['styles'] ),
isset( $args['attributes'] ) ? wc_implode_html_attributes( $args['attributes'] ) : '',
esc_html( $product->add_to_cart_text() )
array(
'{classes}' => esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
'{custom_classes}' => esc_attr( $classname . ' ' . $custom_width_classes . ' ' . $custom_align_classes ),
'{html_element}' => $html_element,
'{add_to_cart_url}' => esc_url( $product->add_to_cart_url() ),
'{button_classes}' => isset( $args['class'] ) ? esc_attr( $args['class'] ) : '',
'{button_styles}' => esc_attr( $styles_and_classes['styles'] ),
'{attributes}' => isset( $args['attributes'] ) ? wc_implode_html_attributes( $args['attributes'] ) : '',
'{add_to_cart_text}' => esc_html( $initial_product_text ),
'{div_directives}' => $is_ajax_button ? $div_directives : '',
'{button_directives}' => $is_ajax_button ? $button_directives : '',
'{span_button_directives}' => $is_ajax_button ? $span_button_directives : '',
'{view_cart_html}' => $is_ajax_button ? $this->get_view_cart_html() : '',
)
),
$product,
$args
);
}
}
/**
* Get the number of items in the cart for a given product id.
*
* @param number $product_id The product id.
* @return number The number of items in the cart.
*/
private function get_cart_item_quantities_by_product_id( $product_id ) {
if ( ! isset( WC()->cart ) ) {
return 0;
}
$cart = WC()->cart->get_cart_item_quantities();
return isset( $cart[ $product_id ] ) ? $cart[ $product_id ] : 0;
}
/**
* Prevent caching on certain pages
*/
private function prevent_cache() {
\WC_Cache_Helper::set_nocache_constants();
nocache_headers();
}
/**
* Get the view cart link html.
*
* @return string The view cart html.
*/
private function get_view_cart_html() {
return sprintf(
'<span hidden data-wc-bind--hidden="!selectors.woocommerce.displayViewCart">
<a
href="%1$s"
class="added_to_cart wc_forward"
title="%2$s"
>
%2$s
</a>
</span>',
wc_get_cart_url(),
__( 'View cart', 'woocommerce' )
);
}
}

View File

@@ -16,6 +16,13 @@ class ProductCollection extends AbstractBlock {
*/
protected $block_name = 'product-collection';
/**
* The Block with its attributes before it gets rendered
*
* @var array
*/
protected $parsed_block;
/**
* All query args from WP_Query.
*
@@ -84,6 +91,102 @@ class ProductCollection extends AbstractBlock {
// Extend allowed `collection_params` for the REST API.
add_filter( 'rest_product_collection_params', array( $this, 'extend_rest_query_allowed_params' ), 10, 1 );
// Interactivity API: Add navigation directives to the product collection block.
add_filter( 'render_block_woocommerce/product-collection', array( $this, 'add_navigation_id_directive' ), 10, 3 );
add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
}
/**
* Mark the Product Collection as an interactive region so it can be updated
* during client-side navigation.
*
* @param string $block_content The block content.
* @param array $block The full block, including name and attributes.
* @param \WP_Block $instance The block instance.
*/
public function add_navigation_id_directive( $block_content, $block, $instance ) {
$is_product_collection_block = $block['attrs']['query']['isProductCollectionBlock'] ?? false;
if ( $is_product_collection_block ) {
// Enqueue the Interactivity API runtime.
wp_enqueue_script( 'wc-interactivity' );
$p = new \WP_HTML_Tag_Processor( $block_content );
// Add `data-wc-navigation-id to the query block.
if ( $p->next_tag( array( 'class_name' => 'wp-block-woocommerce-product-collection' ) ) ) {
$p->set_attribute(
'data-wc-navigation-id',
'wc-product-collection-' . $this->parsed_block['attrs']['queryId']
);
$p->set_attribute( 'data-wc-interactive', true );
$block_content = $p->get_updated_html();
}
}
return $block_content;
}
/**
* Add interactive links to all anchors inside the Query Pagination block.
*
* @param string $block_content The block content.
* @param array $block The full block, including name and attributes.
* @param \WP_Block $instance The block instance.
*/
public function add_navigation_link_directives( $block_content, $block, $instance ) {
$is_product_collection_block = $instance->context['query']['isProductCollectionBlock'] ?? false;
if (
$is_product_collection_block &&
$instance->context['queryId'] === $this->parsed_block['attrs']['queryId']
) {
$p = new \WP_HTML_Tag_Processor( $block_content );
$p->next_tag( array( 'class_name' => 'wp-block-query-pagination' ) );
while ( $p->next_tag( 'a' ) ) {
$class_attr = $p->get_attribute( 'class' );
$class_list = preg_split( '/\s+/', $class_attr );
$is_previous = in_array( 'wp-block-query-pagination-previous', $class_list, true );
$is_next = in_array( 'wp-block-query-pagination-next', $class_list, true );
$is_previous_or_next = $is_previous || $is_next;
$navigation_link_payload = array(
'prefetch' => $is_previous_or_next,
'scroll' => false,
);
$p->set_attribute(
'data-wc-navigation-link',
wp_json_encode( $navigation_link_payload )
);
if ( $is_previous ) {
$p->set_attribute( 'key', 'pagination-previous' );
} elseif ( $is_next ) {
$p->set_attribute( 'key', 'pagination-next' );
}
}
$block_content = $p->get_updated_html();
}
return $block_content;
}
/**
* Extra data passed through from server to client for block.
*
* @param array $attributes Any attributes that currently are available from the block.
* Note, this will be empty in the editor context when the block is
* not in the post content on editor load.
*/
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
// The `loop_shop_per_page` filter can be found in WC_Query::product_query().
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$this->asset_data_registry->add( 'loopShopPerPage', apply_filters( 'loop_shop_per_page', wc_get_default_products_per_row() * wc_get_default_product_rows_per_page() ), true );
}
/**
@@ -132,15 +235,18 @@ class ProductCollection extends AbstractBlock {
$is_product_collection_block = $parsed_block['attrs']['query']['isProductCollectionBlock'] ?? false;
if ( ! $is_product_collection_block ) {
return;
return $pre_render;
}
$this->asset_data_registry->add( 'has_filterable_products', true, true );
$this->parsed_block = $parsed_block;
$this->asset_data_registry->add( 'hasFilterableProducts', true, true );
/**
* It enables the page to refresh when a filter is applied, ensuring that the product collection block,
* which is a server-side rendered (SSR) block, retrieves the products that match the filters.
*/
$this->asset_data_registry->add( 'is_rendering_php_template', true, true );
$this->asset_data_registry->add( 'isRenderingPhpTemplate', true, true );
return $pre_render;
}
/**
@@ -160,6 +266,9 @@ class ProductCollection extends AbstractBlock {
}
$block_context_query = $block->context['query'];
// phpcs:ignore WordPress.DB.SlowDBQuery
$block_context_query['tax_query'] = ! empty( $query['tax_query'] ) ? $query['tax_query'] : array();
return $this->get_final_frontend_query( $block_context_query, $page );
}

View File

@@ -39,10 +39,13 @@ class ProductDetails extends AbstractBlock {
return sprintf(
'<div class="wp-block-woocommerce-product-details %1$s %2$s">
%3$s
<div style="%3$s">
%4$s
</div>
</div>',
esc_attr( $classes_and_styles['classes'] ),
esc_attr( $classname ),
esc_attr( $classes_and_styles['styles'] ),
$tabs
);
}

View File

@@ -1,6 +1,9 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Blocks\Utils\ProductGalleryUtils;
/**
* ProductGallery class.
*/
@@ -13,11 +16,139 @@ class ProductGallery extends AbstractBlock {
protected $block_name = 'product-gallery';
/**
* Get the frontend style handle for this block type.
* Register the context
*
* @return null
* @return string[]
*/
protected function get_block_type_style() {
return null;
protected function get_block_type_uses_context() {
return [ 'postId' ];
}
/**
* Inject dialog into the product gallery HTML.
*
* @param string $gallery_html The gallery HTML.
* @param string $dialog_html The dialog HTML.
*
* @return string
*/
protected function inject_dialog( $gallery_html, $dialog_html ) {
// Find the position of the last </div>.
$pos = strrpos( $gallery_html, '</div>' );
if ( false !== $pos ) {
// Inject the dialog_html at the correct position.
$html = substr_replace( $gallery_html, $dialog_html, $pos, 0 );
return $html;
}
}
/**
* Return the dialog content.
*
* @return string
*/
protected function render_dialog() {
$template_part = BlockTemplateUtils::get_template_part( 'product-gallery' );
$parsed_template = parse_blocks(
$template_part
);
$html = array_reduce(
$parsed_template,
function( $carry, $item ) {
return $carry . render_block( $item );
},
''
);
$html_processor = new \WP_HTML_Tag_Processor( $html );
$html_processor->next_tag(
array(
'class_name' => 'wp-block-woocommerce-product-gallery',
)
);
$html_processor->remove_attribute( 'data-wc-context' );
$gallery_dialog = strtr(
'
<div class="wc-block-product-gallery-dialog__overlay" hidden data-wc-bind--hidden="!selectors.woocommerce.isDialogOpen" data-wc-effect="effects.woocommerce.keyboardAccess">
<dialog data-wc-bind--open="selectors.woocommerce.isDialogOpen">
<div class="wc-block-product-gallery-dialog__header">
<div class="wc-block-product-galler-dialog__header-right">
<button class="wc-block-product-gallery-dialog__close" data-wc-on--click="actions.woocommerce.dialog.handleCloseButtonClick">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" rx="2"/>
<path d="M13 11.8L19.1 5.5L18.1 4.5L12 10.7L5.9 4.5L4.9 5.5L11 11.8L4.5 18.5L5.5 19.5L12 12.9L18.5 19.5L19.5 18.5L13 11.8Z" fill="black"/>
</svg>
</button>
</div>
</div>
<div class="wc-block-product-gallery-dialog__body">
{{html}}
</div>
</dialog>
</div>',
array(
'{{html}}' => $html_processor->get_updated_html(),
)
);
return $gallery_dialog;
}
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
$post_id = $block->context['postId'] ?? '';
$product_gallery_images = ProductGalleryUtils::get_product_gallery_images( $post_id, 'thumbnail', array() );
$classname_single_image = '';
// This is a temporary solution. We have to refactor this code when the block will have to be addable on every page/post https://github.com/woocommerce/woocommerce-blocks/issues/10882.
global $product;
if ( count( $product_gallery_images ) < 2 ) {
// The gallery consists of a single image.
$classname_single_image = 'is-single-product-gallery-image';
}
$number_of_thumbnails = $block->attributes['thumbnailsNumberOfThumbnails'] ?? 0;
$classname = $attributes['className'] ?? '';
$dialog = ( true === $attributes['fullScreenOnClick'] && isset( $attributes['mode'] ) && 'full' !== $attributes['mode'] ) ? $this->render_dialog() : '';
$post_id = $block->context['postId'] ?? '';
$product = wc_get_product( $post_id );
$html = $this->inject_dialog( $content, $dialog );
$p = new \WP_HTML_Tag_Processor( $html );
if ( $p->next_tag() ) {
$p->set_attribute( 'data-wc-interactive', true );
$p->set_attribute(
'data-wc-context',
wp_json_encode(
array(
'woocommerce' => array(
'selectedImage' => $product->get_image_id(),
'visibleImagesIds' => ProductGalleryUtils::get_product_gallery_image_ids( $product, $number_of_thumbnails, true ),
'isDialogOpen' => false,
),
)
)
);
$p->add_class( $classname );
$p->add_class( $classname_single_image );
$html = $p->get_updated_html();
}
return $html;
}
}

View File

@@ -0,0 +1,211 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\ProductGalleryUtils;
/**
* ProductGalleryLargeImage class.
*/
class ProductGalleryLargeImage extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-gallery-large-image';
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Register the context
*
* @return string[]
*/
protected function get_block_type_uses_context() {
return [ 'postId', 'hoverZoom', 'fullScreenOnClick' ];
}
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
* @param array $attributes Any attributes that currently are available from the block.
* @param string $content The block content.
* @param WP_Block $block The block object.
*/
protected function enqueue_assets( array $attributes, $content, $block ) {
if ( $block->context['hoverZoom'] || $block->context['fullScreenOnClick'] ) {
parent::enqueue_assets( $attributes, $content, $block );
}
}
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
$post_id = $block->context['postId'];
if ( ! isset( $post_id ) ) {
return '';
}
global $product;
$previous_product = $product;
$product = wc_get_product( $post_id );
if ( ! $product instanceof \WC_Product ) {
$product = $previous_product;
return '';
}
if ( class_exists( 'WC_Frontend_Scripts' ) ) {
$frontend_scripts = new \WC_Frontend_Scripts();
$frontend_scripts::load_scripts();
}
$processor = new \WP_HTML_Tag_Processor( $content );
$processor->next_tag();
$processor->remove_class( 'wp-block-woocommerce-product-gallery-large-image' );
$content = $processor->get_updated_html();
[ $visible_main_image, $main_images ] = $this->get_main_images_html( $block->context, $post_id );
$directives = $this->get_directives( $block->context );
return strtr(
'<div class="wc-block-product-gallery-large-image wp-block-woocommerce-product-gallery-large-image" {directives}>
<div class="wc-block-product-gallery-large-image__container">
{main_images}
</div>
{content}
</div>',
array(
'{visible_main_image}' => $visible_main_image,
'{main_images}' => implode( ' ', $main_images ),
'{content}' => $content,
'{directives}' => array_reduce(
array_keys( $directives ),
function( $carry, $key ) use ( $directives ) {
return $carry . ' ' . $key . '="' . esc_attr( $directives[ $key ] ) . '"';
},
''
),
)
);
}
/**
* Get the main images html code. The first element of the array contains the HTML of the first image that is visible, the second element contains the HTML of the other images that are hidden.
*
* @param array $context The block context.
* @param int $product_id The product id.
*
* @return array
*/
private function get_main_images_html( $context, $product_id ) {
$attributes = array(
'data-wc-bind--style' => 'selectors.woocommerce.productGalleryLargeImage.styles',
'data-wc-effect' => 'effects.woocommerce.scrollInto',
'class' => 'wc-block-woocommerce-product-gallery-large-image__image',
);
if ( $context['fullScreenOnClick'] ) {
$attributes['class'] .= ' wc-block-woocommerce-product-gallery-large-image__image--full-screen-on-click';
}
if ( $context['hoverZoom'] ) {
$attributes['class'] .= ' wc-block-woocommerce-product-gallery-large-image__image--hoverZoom';
$attributes['data-wc-bind--style'] = 'selectors.woocommerce.productGalleryLargeImage.styles';
}
$main_images = ProductGalleryUtils::get_product_gallery_images(
$product_id,
'full',
$attributes,
'wc-block-product-gallery-large-image__image-element'
);
$main_image_with_wrapper = array_map(
function( $main_image_element ) {
return "<div class='wc-block-product-gallery-large-image__wrapper'>" . $main_image_element . '</div>';
},
$main_images
);
$visible_main_image = array_shift( $main_images );
return array( $visible_main_image, $main_image_with_wrapper );
}
/**
* Get directives for the block.
*
* @param array $block_context The block context.
*
* @return array
*/
private function get_directives( $block_context ) {
return array_merge(
$this->get_zoom_directives( $block_context ),
$this->get_open_dialog_directives( $block_context )
);
}
/**
* Get directives for zoom.
*
* @param array $block_context The block context.
*
* @return array
*/
private function get_zoom_directives( $block_context ) {
if ( ! $block_context['hoverZoom'] ) {
return array();
}
$context = array(
'woocommerce' => array(
'styles' => array(
'transform' => 'scale(1.0)',
'transform-origin' => '',
),
),
);
return array(
'data-wc-on--mousemove' => 'actions.woocommerce.handleMouseMove',
'data-wc-on--mouseleave' => 'actions.woocommerce.handleMouseLeave',
'data-wc-context' => wp_json_encode( $context, JSON_NUMERIC_CHECK ),
);
}
/**
* Get directives for opening the dialog.
*
* @param array $block_context The block context.
*
* @return array
*/
private function get_open_dialog_directives( $block_context ) {
if ( ! $block_context['fullScreenOnClick'] ) {
return array();
}
return array(
'data-wc-on--click' => 'actions.woocommerce.handleClick',
);
}
}

View File

@@ -0,0 +1,221 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* ProductGalleryLargeImage class.
*/
class ProductGalleryLargeImageNextPrevious extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-gallery-large-image-next-previous';
/**
* It isn't necessary register block assets because it is a server side block.
*/
protected function register_block_type_assets() {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Register the context
*
* @return string[]
*/
protected function get_block_type_uses_context() {
return [ 'nextPreviousButtonsPosition', 'productGalleryClientId' ];
}
/**
* Return class suffix
*
* @param array $context Block context.
* @return string
*/
private function get_class_suffix( $context ) {
switch ( $context['nextPreviousButtonsPosition'] ) {
case 'insideTheImage':
return 'inside-image';
case 'outsideTheImage':
return 'outside-image';
case 'off':
return 'off';
default:
return 'off';
}
}
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
$post_id = $block->context['postId'];
if ( ! isset( $post_id ) ) {
return '';
}
$product = wc_get_product( $post_id );
$product_gallery = $product->get_gallery_image_ids();
if ( empty( $product_gallery ) ) {
return null;
}
$context = $block->context;
$prev_button = $this->get_button( 'previous', $context );
$p = new \WP_HTML_Tag_Processor( $prev_button );
if ( $p->next_tag() ) {
$p->set_attribute(
'data-wc-on--click',
'actions.woocommerce.handlePreviousImageButtonClick'
);
$prev_button = $p->get_updated_html();
}
$next_button = $this->get_button( 'next', $context );
$p = new \WP_HTML_Tag_Processor( $next_button );
if ( $p->next_tag() ) {
$p->set_attribute(
'data-wc-on--click',
'actions.woocommerce.handleNextImageButtonClick'
);
$next_button = $p->get_updated_html();
}
$alignment_class = isset( $attributes['layout']['verticalAlignment'] ) ? 'is-vertically-aligned-' . $attributes['layout']['verticalAlignment'] : '';
$position_class = 'wc-block-product-gallery-large-image-next-previous--' . $this->get_class_suffix( $context );
return strtr(
'<div class="wc-block-product-gallery-large-image-next-previous wp-block-woocommerce-product-gallery-large-image-next-previous {alignment_class}">
<div class="wc-block-product-gallery-large-image-next-previous-container {position_class}">
{prev_button}
{next_button}
</div>
</div>',
array(
'{prev_button}' => $prev_button,
'{next_button}' => $next_button,
'{alignment_class}' => $alignment_class,
'{position_class}' => $position_class,
)
);
}
/**
* Generates the HTML for a next or previous button for the product gallery large image.
*
* @param string $button_type The type of button to generate. Either 'previous' or 'next'.
* @param string $context The block context.
* @return string The HTML for the generated button.
*/
protected function get_button( $button_type, $context ) {
if ( 'insideTheImage' === $context['nextPreviousButtonsPosition'] ) {
return $this->get_inside_button( $button_type, $context );
}
return $this->get_outside_button( $button_type, $context );
}
/**
* Returns an HTML button element with an SVG icon for the previous or next button when the buttons are inside the image.
*
* @param string $button_type The type of button to return. Either "previous" or "next".
* @param string $context The context in which the button is being used.
* @return string The HTML for the button element.
*/
protected function get_inside_button( $button_type, $context ) {
$previous_button_icon_path = 'M28.1 12L30.5 14L21.3 24L30.5 34L28.1 36L17.3 24L28.1 12Z';
$next_button_icon_path = 'M21.7001 12L19.3 14L28.5 24L19.3 34L21.7001 36L32.5 24L21.7001 12Z';
$icon_path = $previous_button_icon_path;
$button_side_class = 'left';
if ( 'next' === $button_type ) {
$icon_path = $next_button_icon_path;
$button_side_class = 'right';
}
return sprintf(
'<button class="wc-block-product-gallery-large-image-next-previous--button wc-block-product-gallery-large-image-next-previous-%1$s--%2$s">
<svg xmlns="http://www.w3.org/2000/svg" width="49" height="48" viewBox="0 0 49 48" fill="none">
<g filter="url(#filter0_b_397_11354)">
<rect x="0.5" width="48" height="48" rx="5" fill="black" fill-opacity="0.5"/>
<path d="%3$s" fill="white"/>
</g>
<defs>
<filter id="filter0_b_397_11354" x="-9.5" y="-10" width="68" height="68" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feGaussianBlur in="BackgroundImageFix" stdDeviation="5"/>
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_397_11354"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_397_11354" result="shape"/>
</filter>
</defs>
</svg>
</button>',
$button_side_class,
$this->get_class_suffix( $context ),
$icon_path
);
}
/**
* Returns an HTML button element with an SVG icon for the previous or next button when the buttons are outside the image.
*
* @param string $button_type The type of button to return. Either "previous" or "next".
* @param string $context The context in which the button is being used.
* @return string The HTML for the button element.
*/
protected function get_outside_button( $button_type, $context ) {
$next_button_icon_path = 'M4.56666 0L0.766663 3.16667L15.3333 19L0.766663 34.8333L4.56666 38L21.6667 19L4.56666 0Z';
$previous_button_icon_path = 'M17.7 0L21.5 3.16667L6.93334 19L21.5 34.8333L17.7 38L0.600002 19L17.7 0Z';
$icon_path = $previous_button_icon_path;
$button_side_class = 'left';
if ( 'next' === $button_type ) {
$icon_path = $next_button_icon_path;
$button_side_class = 'right';
}
return sprintf(
'<button class="wc-block-product-gallery-large-image-next-previous--button wc-block-product-gallery-large-image-next-previous-%1$s--%2$s">
<svg
width="22"
height="38"
viewBox="0 0 22 38"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="%3$s"
fill="black"
/>
</svg>
</button>',
$button_side_class,
$this->get_class_suffix( $context ),
$icon_path
);
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\ProductGalleryUtils;
/**
* ProductGalleryPager class.
*/
class ProductGalleryPager extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-gallery-pager';
/**
* It isn't necessary register block assets because it is a server side block.
*/
protected function register_block_type_assets() {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Register the context
*
* @return string[]
*/
protected function get_block_type_uses_context() {
return [ 'productGalleryClientId', 'pagerDisplayMode', 'thumbnailsNumberOfThumbnails', 'postId' ];
}
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
$pager_display_mode = $block->context['pagerDisplayMode'] ?? '';
if ( 'off' === $pager_display_mode ) {
return null;
}
$number_of_thumbnails = $block->context['thumbnailsNumberOfThumbnails'] ?? 0;
$classname = $attributes['className'] ?? '';
$wrapper_attributes = get_block_wrapper_attributes( array( 'class' => trim( $classname ) ) );
$post_id = $block->context['postId'] ?? '';
$product = wc_get_product( $post_id );
if ( $product ) {
$product_gallery_images_ids = ProductGalleryUtils::get_product_gallery_image_ids( $product );
$number_of_available_images = count( $product_gallery_images_ids );
$number_of_thumbnails = $number_of_thumbnails < $number_of_available_images ? $number_of_thumbnails : $number_of_available_images;
if ( $number_of_thumbnails > 1 ) {
$html = $this->render_pager( $product_gallery_images_ids, $pager_display_mode, $number_of_thumbnails );
return sprintf(
'<div %1$s>
%2$s
</div>',
$wrapper_attributes,
$html
);
}
return '';
}
}
/**
* Renders the pager for the product gallery.
*
* @param array $product_gallery_images_ids An array of image IDs for the product gallery.
* @param string $pager_display_mode The display mode for the pager.
* @param int $number_of_thumbnails The number of thumbnails to display in the pager.
* @return string|null The rendered pager HTML, or null if the pager should not be displayed.
*/
private function render_pager( $product_gallery_images_ids, $pager_display_mode, $number_of_thumbnails ) {
if ( $number_of_thumbnails < 2 || 'off' === $pager_display_mode ) {
return null;
}
return $this->render_pager_pages( $product_gallery_images_ids, $number_of_thumbnails, $pager_display_mode );
}
/**
* Renders the pager pages for the product gallery.
*
* @param array $product_gallery_images_ids An array of image IDs for the product gallery.
* @param int $number_of_thumbnails The number of thumbnails to display in the pager.
* @param string $pager_display_mode The display mode for the pager. Defaults to 'dots'.
* @return string The rendered pager pages HTML.
*/
private function render_pager_pages( $product_gallery_images_ids, $number_of_thumbnails, $pager_display_mode = 'dots' ) {
$html = '';
foreach ( $product_gallery_images_ids as $key => $product_gallery_image_id ) {
if ( $key >= $number_of_thumbnails ) {
break;
}
$is_first_pager_item = 0 === $key;
$pager_item = sprintf(
'<li class="wc-block-product-gallery-pager__pager-item %2$s">%1$s</li>',
'dots' === $pager_display_mode ? $this->get_dot_icon( $is_first_pager_item ) : $key + 1,
$is_first_pager_item ? 'wc-block-product-gallery-pager__pager-item--is-active' : ''
);
$p = new \WP_HTML_Tag_Processor( $pager_item );
if ( $p->next_tag() ) {
$p->set_attribute(
'data-wc-context',
wp_json_encode(
array(
'woocommerce' => array( 'imageId' => $product_gallery_image_id ),
)
)
);
$p->set_attribute(
'data-wc-on--click',
'actions.woocommerce.handleSelectImage'
);
$p->set_attribute(
'data-wc-class--wc-block-product-gallery-pager__pager-item--is-active',
'selectors.woocommerce.isSelected'
);
$html .= $p->get_updated_html();
}
}
return sprintf(
'<ul class="wc-block-product-gallery-pager__pager">
%1$s
</ul>',
$html
);
}
/**
* Generates an SVG dot icon with the specified opacity.
*
* @param bool $is_active Whether the dot icon should be in active state. Defaults to false.
* @return string The SVG dot icon HTML.
*/
private function get_dot_icon( $is_active = false ) {
$initial_opacity = $is_active ? '1' : '0.2';
return sprintf(
'<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="6" cy="6" r="6" fill="black" fill-opacity="%1$s" data-wc-bind--fill-opacity="selectors.woocommerce.pagerDotFillOpacity" />
</svg>',
$initial_opacity
);
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
use Automattic\WooCommerce\Blocks\Utils\ProductGalleryUtils;
/**
* ProductGalleryThumbnails class.
*/
class ProductGalleryThumbnails extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-gallery-thumbnails';
/**
* It isn't necessary register block assets because it is a server side block.
*/
protected function register_block_type_assets() {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Register the context
*
* @return string[]
*/
protected function get_block_type_uses_context() {
return [ 'productGalleryClientId', 'postId', 'thumbnailsNumberOfThumbnails', 'thumbnailsPosition' ];
}
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( isset( $block->context['thumbnailsPosition'] ) && '' !== $block->context['thumbnailsPosition'] && 'off' !== $block->context['thumbnailsPosition'] ) {
if ( ! empty( $content ) ) {
parent::register_block_type_assets();
$this->register_chunk_translations( [ $this->block_name ] );
return $content;
}
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$post_id = $block->context['postId'] ?? '';
$product = wc_get_product( $post_id );
if ( $product ) {
$post_thumbnail_id = $product->get_image_id();
$product_gallery_images = ProductGalleryUtils::get_product_gallery_images( $post_id, 'thumbnail', array(), 'wc-block-product-gallery-thumbnails__thumbnail' );
if ( $product_gallery_images && $post_thumbnail_id ) {
$html = '';
$number_of_thumbnails = isset( $block->context['thumbnailsNumberOfThumbnails'] ) ? $block->context['thumbnailsNumberOfThumbnails'] : 3;
$thumbnails_count = 1;
foreach ( $product_gallery_images as $product_gallery_image_html ) {
if ( $thumbnails_count > $number_of_thumbnails ) {
break;
}
$processor = new \WP_HTML_Tag_Processor( $product_gallery_image_html );
if ( $processor->next_tag( 'img' ) ) {
$processor->set_attribute(
'data-wc-on--click',
'actions.woocommerce.thumbnails.handleClick'
);
$html .= $processor->get_updated_html();
}
$thumbnails_count++;
}
return sprintf(
'<div class="wc-block-product-gallery-thumbnails wp-block-woocommerce-product-gallery-thumbnails %1$s" style="%2$s">
%3$s
</div>',
esc_attr( $classes_and_styles['classes'] ),
esc_attr( $classes_and_styles['styles'] ),
$html
);
}
}
return;
}
}
}

View File

@@ -105,7 +105,7 @@ class ProductImage extends AbstractBlock {
<span class="screen-reader-text">Product on sale</span>
</div>
',
$attributes['saleBadgeAlign'],
esc_attr( $attributes['saleBadgeAlign'] ),
isset( $font_size['class'] ) ? esc_attr( $font_size['class'] ) : '',
isset( $font_size['style'] ) ? esc_attr( $font_size['style'] ) : '',
esc_html__( 'Sale', 'woocommerce' )
@@ -158,6 +158,9 @@ class ProductImage extends AbstractBlock {
if ( ! empty( $attributes['scale'] ) ) {
$image_style .= sprintf( 'object-fit:%s;', $attributes['scale'] );
}
if ( ! empty( $attributes['aspectRatio'] ) ) {
$image_style .= sprintf( 'aspect-ratio:%s;', $attributes['aspectRatio'] );
}
if ( ! $product->get_image_id() ) {
// The alt text is left empty on purpose, as it's considered a decorative image.
@@ -190,7 +193,7 @@ class ProductImage extends AbstractBlock {
* not in the post content on editor load.
*/
protected function enqueue_data( array $attributes = [] ) {
$this->asset_data_registry->add( 'is_block_theme_enabled', wc_current_theme_is_fse_theme(), false );
$this->asset_data_registry->add( 'isBlockThemeEnabled', wc_current_theme_is_fse_theme(), false );
}
@@ -210,11 +213,9 @@ class ProductImage extends AbstractBlock {
}
$parsed_attributes = $this->parse_attributes( $attributes );
$border_radius = StyleAttributesUtils::get_border_radius_class_and_style( $attributes );
$margin = StyleAttributesUtils::get_margin_class_and_style( $attributes );
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$post_id = $block->context['postId'];
$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
$product = wc_get_product( $post_id );
if ( $product ) {

View File

@@ -87,7 +87,7 @@ class ProductPrice extends AbstractBlock {
return $content;
}
$post_id = $block->context['postId'];
$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
$product = wc_get_product( $post_id );
if ( $product ) {

View File

@@ -72,6 +72,12 @@ class ProductQuery extends AbstractBlock {
10,
2
);
add_filter(
'render_block',
array( $this, 'enqueue_styles' ),
10,
2
);
add_filter( 'rest_product_query', array( $this, 'update_rest_query' ), 10, 2 );
add_filter( 'rest_product_collection_params', array( $this, 'extend_rest_query_allowed_params' ), 10, 1 );
}
@@ -120,9 +126,13 @@ class ProductQuery extends AbstractBlock {
$post_template_has_support_for_grid_view = $this->check_if_post_template_has_support_for_grid_view();
$this->asset_data_registry->add(
'post_template_has_support_for_grid_view',
'postTemplateHasSupportForGridView',
$post_template_has_support_for_grid_view
);
// The `loop_shop_per_page` filter can be found in WC_Query::product_query().
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$this->asset_data_registry->add( 'loopShopPerPage', apply_filters( 'loop_shop_per_page', wc_get_default_products_per_row() * wc_get_default_product_rows_per_page() ), true );
}
/**
@@ -136,6 +146,22 @@ class ProductQuery extends AbstractBlock {
&& substr( $parsed_block['attrs']['namespace'], 0, 11 ) === 'woocommerce';
}
/**
* Enqueues the variation styles when rendering the Product Query variation.
*
* @param string $block_content The block content.
* @param array $block The full block, including name and attributes.
*
* @return string The block content.
*/
public function enqueue_styles( string $block_content, array $block ) {
if ( 'core/query' === $block['blockName'] && self::is_woocommerce_variation( $block ) ) {
wp_enqueue_style( 'wc-blocks-style-product-query' );
}
return $block_content;
}
/**
* Update the query for the product query block.
*
@@ -144,15 +170,15 @@ class ProductQuery extends AbstractBlock {
*/
public function update_query( $pre_render, $parsed_block ) {
if ( 'core/query' !== $parsed_block['blockName'] ) {
return;
return $pre_render;
}
$this->parsed_block = $parsed_block;
if ( self::is_woocommerce_variation( $parsed_block ) ) {
// Set this so that our product filters can detect if it's a PHP template.
$this->asset_data_registry->add( 'has_filterable_products', true, true );
$this->asset_data_registry->add( 'is_rendering_php_template', true, true );
$this->asset_data_registry->add( 'hasFilterableProducts', true, true );
$this->asset_data_registry->add( 'isRenderingPhpTemplate', true, true );
add_filter(
'query_loop_block_query_vars',
array( $this, 'build_query' ),
@@ -160,6 +186,8 @@ class ProductQuery extends AbstractBlock {
1
);
}
return $pre_render;
}
/**

View File

@@ -86,7 +86,7 @@ class ProductRating extends AbstractBlock {
* @return null
*/
protected function get_block_type_style() {
return null;
return array_merge( parent::get_block_type_style(), [ 'wc-blocks-packages-style' ] );
}
/**

View File

@@ -0,0 +1,209 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* ProductRatingCounter class.
*/
class ProductRatingCounter extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-rating-counter';
/**
* API version name.
*
* @var string
*/
protected $api_version = '2';
/**
* Get block supports. Shared with the frontend.
* IMPORTANT: If you change anything here, make sure to update the JS file too.
*
* @return array
*/
protected function get_block_type_supports() {
return array(
'color' =>
array(
'text' => true,
'background' => false,
'link' => false,
'__experimentalSkipSerialization' => true,
),
'typography' =>
array(
'fontSize' => true,
'__experimentalSkipSerialization' => true,
),
'spacing' =>
array(
'margin' => true,
'padding' => true,
'__experimentalSkipSerialization' => true,
),
'__experimentalSelector' => '.wc-block-components-product-rating-counter',
);
}
/**
* Get the block's attributes.
*
* @param array $attributes Block attributes. Default empty array.
* @return array Block attributes merged with defaults.
*/
private function parse_attributes( $attributes ) {
// These should match what's set in JS `registerBlockType`.
$defaults = array(
'productId' => 0,
'isDescendentOfQueryLoop' => false,
'textAlign' => '',
'isDescendentOfSingleProductBlock' => false,
'isDescendentOfSingleProductTemplate' => false,
);
return wp_parse_args( $attributes, $defaults );
}
/**
* Overwrite parent method to prevent script registration.
*
* It is necessary to register and enqueues assets during the render
* phase because we want to load assets only if the block has the content.
*/
protected function register_block_type_assets() {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Register the context.
*/
protected function get_block_type_uses_context() {
return [ 'query', 'queryId', 'postId' ];
}
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( ! empty( $content ) ) {
parent::register_block_type_assets();
$this->register_chunk_translations( [ $this->block_name ] );
return $content;
}
$post_id = $block->context['postId'];
$product = wc_get_product( $post_id );
if ( $product && $product->get_review_count() > 0 ) {
$product_reviews_count = $product->get_review_count();
$product_rating = $product->get_average_rating();
$parsed_attributes = $this->parse_attributes( $attributes );
$is_descendent_of_single_product_block = $parsed_attributes['isDescendentOfSingleProductBlock'];
$is_descendent_of_single_product_template = $parsed_attributes['isDescendentOfSingleProductTemplate'];
$styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$text_align_styles_and_classes = StyleAttributesUtils::get_text_align_class_and_style( $attributes );
/**
* Filter the output from wc_get_rating_html.
*
* @param string $html Star rating markup. Default empty string.
* @param float $rating Rating being shown.
* @param int $count Total number of ratings.
* @return string
*/
$filter_rating_html = function( $html, $rating, $count ) use ( $post_id, $product_rating, $product_reviews_count, $is_descendent_of_single_product_block, $is_descendent_of_single_product_template ) {
$product_permalink = get_permalink( $post_id );
$reviews_count = $count;
$average_rating = $rating;
if ( $product_rating ) {
$average_rating = $product_rating;
}
if ( $product_reviews_count ) {
$reviews_count = $product_reviews_count;
}
if ( 0 < $average_rating || false === $product_permalink ) {
/* translators: %s: rating */
$customer_reviews_count = sprintf(
/* translators: %s is referring to the total of reviews for a product */
_n(
'(%s customer review)',
'(%s customer reviews)',
$reviews_count,
'woocommerce'
),
esc_html( $reviews_count )
);
if ( $is_descendent_of_single_product_block ) {
$customer_reviews_count = '<a href="' . esc_url( $product_permalink ) . '#reviews">' . $customer_reviews_count . '</a>';
} elseif ( $is_descendent_of_single_product_template ) {
$customer_reviews_count = '<a class="woocommerce-review-link" rel="nofollow" href="#reviews">' . $customer_reviews_count . '</a>';
}
$html = sprintf(
'<div class="wc-block-components-product-rating-counter__container">
<span class="wc-block-components-product-rating-counter__reviews_count">%1$s</span>
</div>
',
$customer_reviews_count
);
} else {
$html = '';
}
return $html;
};
add_filter(
'woocommerce_product_get_rating_html',
$filter_rating_html,
10,
3
);
$rating_html = wc_get_rating_html( $product->get_average_rating() );
remove_filter(
'woocommerce_product_get_rating_html',
$filter_rating_html,
10
);
return sprintf(
'<div class="wc-block-components-product-rating-counter wc-block-grid__product-rating-counter %1$s %2$s" style="%3$s">
%4$s
</div>',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
esc_attr( $styles_and_classes['classes'] ),
esc_attr( $styles_and_classes['styles'] ?? '' ),
$rating_html
);
}
return '';
}
}

View File

@@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* ProductRating class.
* ProductRatingStars class.
*/
class ProductRatingStars extends AbstractBlock {
@@ -47,7 +47,7 @@ class ProductRatingStars extends AbstractBlock {
'padding' => true,
'__experimentalSkipSerialization' => true,
),
'__experimentalSelector' => '.wc-block-components-product-rating',
'__experimentalSelector' => '.wc-block-components-product-rating-stars',
);
}

View File

@@ -45,7 +45,7 @@ class ProductResultsCount extends AbstractBlock {
return sprintf(
'<div class="woocommerce wc-block-product-results-count wp-block-woocommerce-product-results-count %1$s %2$s" style="%3$s">%4$s</div>',
esc_attr( $classes_and_styles['classes'] ),
$classname,
esc_attr( $classname ),
esc_attr( $classes_and_styles['styles'] ),
$product_results_count
);

View File

@@ -58,8 +58,10 @@ class ProductSaleBadge extends AbstractBlock {
'margin' => true,
'padding' => true,
'__experimentalSkipSerialization' => true,
),
'__experimentalSelector' => '.wc-block-components-product-sale-badge',
);
}
@@ -106,16 +108,15 @@ class ProductSaleBadge extends AbstractBlock {
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$classname = isset( $attributes['className'] ) ? $attributes['className'] : '';
$output = '<div class="wc-block-components-product-sale-badge '
. esc_attr( $classes_and_styles['classes'] ) . ' '
. esc_attr( $classname ) . '" '
. 'style="' . esc_attr( $classes_and_styles['styles'] ) . '"'
. '>';
$align = isset( $attributes['align'] ) ? $attributes['align'] : '';
$output = '<div class="wp-block-woocommerce-product-sale-badge ' . esc_attr( $classname ) . '">';
$output .= sprintf( '<div class="wc-block-components-product-sale-badge %1$s wc-block-components-product-sale-badge--align-%2$s" style="%3$s">', esc_attr( $classes_and_styles['classes'] ), esc_attr( $align ), esc_attr( $classes_and_styles['styles'] ) );
$output .= '<span class="wc-block-components-product-sale-badge__text" aria-hidden="true">' . __( 'Sale', 'woocommerce' ) . '</span>';
$output .= '<span class="screen-reader-text">'
. __( 'Product on sale', 'woocommerce' )
. '</span>';
$output .= '</div>';
$output .= '</div></div>';
return $output;
}

View File

@@ -61,7 +61,11 @@ class ProductTemplate extends AbstractBlock {
$classnames = '';
if ( isset( $block->context['displayLayout'] ) && isset( $block->context['query'] ) ) {
if ( isset( $block->context['displayLayout']['type'] ) && 'flex' === $block->context['displayLayout']['type'] ) {
$classnames = "is-flex-container columns-{$block->context['displayLayout']['columns']}";
if ( isset( $block->context['displayLayout']['shrinkColumns'] ) && $block->context['displayLayout']['shrinkColumns'] ) {
$classnames = "wc-block-product-template__responsive columns-{$block->context['displayLayout']['columns']}";
} else {
$classnames = "is-flex-container columns-{$block->context['displayLayout']['columns']}";
}
}
}
if ( isset( $attributes['style']['elements']['link']['color']['text'] ) ) {
@@ -97,7 +101,7 @@ class ProductTemplate extends AbstractBlock {
// Wrap the render inner blocks in a `li` element with the appropriate post classes.
$post_classes = implode( ' ', get_post_class( 'wc-block-product' ) );
$content .= '<li class="' . esc_attr( $post_classes ) . '">' . $block_content . '</li>';
$content .= '<li data-wc-key="product-item-' . get_the_ID() . '" class="' . esc_attr( $post_classes ) . '">' . $block_content . '</li>';
}
/*

View File

@@ -68,7 +68,7 @@ class RelatedProducts extends AbstractBlock {
*/
public function update_query( $pre_render, $parsed_block ) {
if ( 'core/query' !== $parsed_block['blockName'] ) {
return;
return $pre_render;
}
$this->parsed_block = $parsed_block;
@@ -82,6 +82,8 @@ class RelatedProducts extends AbstractBlock {
1
);
}
return $pre_render;
}
/**

View File

@@ -64,31 +64,6 @@ class SingleProduct extends AbstractBlock {
return $block_content;
}
/**
* Render the Single Product block
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
*
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
$classname = $attributes['className'] ?? '';
$wrapper_attributes = get_block_wrapper_attributes( array( 'class' => trim( sprintf( 'woocommerce %1$s', $classname ) ) ) );
$html = sprintf(
'<div %1$s>
%2$s
</div>',
$wrapper_attributes,
$content
);
return $html;
}
/**
* Update the context by injecting the correct post data
* for each one of the Single Product inner blocks.

View File

@@ -26,6 +26,17 @@ class StoreNotices extends AbstractBlock {
* @return string | void Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
/**
* This block should be rendered only on the frontend. Woo loads notice
* functions on the front end requests only. So it's safe and handy to
* check for the print notice function existence to short circuit the
* render process on the admin side.
* See WooCommerce::is_request() for the frontend request definition.
*/
if ( ! function_exists( 'wc_print_notices' ) ) {
return $content;
}
ob_start();
woocommerce_output_all_notices();
$notices = ob_get_clean();

View File

@@ -1,7 +1,6 @@
<?php
namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Blocks\BlockTypes\AtomicBlock;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
@@ -52,6 +51,7 @@ final class BlockTypesController {
add_filter( 'render_block', array( $this, 'add_data_attributes' ), 10, 2 );
add_action( 'woocommerce_login_form_end', array( $this, 'redirect_to_field' ) );
add_filter( 'widget_types_to_hide_from_legacy_widget_block', array( $this, 'hide_legacy_widgets_with_block_equivalent' ) );
add_action( 'woocommerce_delete_product_transients', array( $this, 'delete_product_transients' ) );
}
/**
@@ -61,10 +61,10 @@ final class BlockTypesController {
$block_types = $this->get_block_types();
foreach ( $block_types as $block_type ) {
$block_type_class = __NAMESPACE__ . '\\BlockTypes\\' . $block_type;
$block_type_instance = new $block_type_class( $this->asset_api, $this->asset_data_registry, new IntegrationRegistry() );
}
$block_type_class = __NAMESPACE__ . '\\BlockTypes\\' . $block_type;
new $block_type_class( $this->asset_api, $this->asset_data_registry, new IntegrationRegistry() );
}
}
/**
@@ -160,6 +160,13 @@ final class BlockTypesController {
return $widget_types;
}
/**
* Delete product transients when a product is deleted.
*/
public function delete_product_transients() {
delete_transient( 'wc_blocks_has_downloadable_product' );
}
/**
* Get list of block types.
*
@@ -177,6 +184,7 @@ final class BlockTypesController {
'Breadcrumbs',
'CatalogSorting',
'ClassicTemplate',
'ClassicShortcode',
'CustomerAccount',
'FeaturedCategory',
'FeaturedProduct',
@@ -190,14 +198,18 @@ final class BlockTypesController {
'ProductButton',
'ProductCategories',
'ProductCategory',
'ProductGallery',
'ProductCollection',
'ProductImage',
'ProductImageGallery',
'ProductNew',
'ProductOnSale',
'ProductPrice',
'ProductTemplate',
'ProductQuery',
'ProductAverageRating',
'ProductRating',
'ProductRatingCounter',
'ProductRatingStars',
'ProductResultsCount',
'ProductReviews',
'ProductSaleBadge',
@@ -216,6 +228,18 @@ final class BlockTypesController {
'ProductDetails',
'SingleProduct',
'StockFilter',
'PageContentWrapper',
'OrderConfirmation\Status',
'OrderConfirmation\Summary',
'OrderConfirmation\Totals',
'OrderConfirmation\TotalsWrapper',
'OrderConfirmation\Downloads',
'OrderConfirmation\DownloadsWrapper',
'OrderConfirmation\BillingAddress',
'OrderConfirmation\ShippingAddress',
'OrderConfirmation\BillingWrapper',
'OrderConfirmation\ShippingWrapper',
'OrderConfirmation\AdditionalInformation',
];
$block_types = array_merge(
@@ -226,9 +250,13 @@ final class BlockTypesController {
);
if ( Package::feature()->is_experimental_build() ) {
$block_types[] = 'ProductCollection';
$block_types[] = 'ProductRatingStars';
$block_types[] = 'ProductTemplate';
$block_types[] = 'ProductGallery';
$block_types[] = 'ProductGalleryLargeImage';
$block_types[] = 'ProductGalleryLargeImageNextPrevious';
$block_types[] = 'ProductGalleryPager';
$block_types[] = 'ProductGalleryThumbnails';
$block_types[] = 'CollectionFilters';
$block_types[] = 'CollectionPriceFilter';
}
/**
@@ -258,7 +286,17 @@ final class BlockTypesController {
'ClassicTemplate',
'ProductResultsCount',
'ProductDetails',
'StoreNotices',
'OrderConfirmation\Status',
'OrderConfirmation\Summary',
'OrderConfirmation\Totals',
'OrderConfirmation\TotalsWrapper',
'OrderConfirmation\Downloads',
'OrderConfirmation\DownloadsWrapper',
'OrderConfirmation\BillingAddress',
'OrderConfirmation\ShippingAddress',
'OrderConfirmation\BillingWrapper',
'OrderConfirmation\ShippingWrapper',
'OrderConfirmation\AdditionalInformation',
]
);
}

View File

@@ -8,10 +8,12 @@ use Automattic\WooCommerce\Blocks\BlockPatterns;
use Automattic\WooCommerce\Blocks\BlockTemplatesController;
use Automattic\WooCommerce\Blocks\BlockTypesController;
use Automattic\WooCommerce\Blocks\Domain\Services\CreateAccount;
use Automattic\WooCommerce\Blocks\Domain\Services\JetpackWooCommerceAnalytics;
use Automattic\WooCommerce\Blocks\Domain\Services\Notices;
use Automattic\WooCommerce\Blocks\Domain\Services\DraftOrders;
use Automattic\WooCommerce\Blocks\Domain\Services\FeatureGating;
use Automattic\WooCommerce\Blocks\Domain\Services\GoogleAnalytics;
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
use Automattic\WooCommerce\Blocks\InboxNotifications;
use Automattic\WooCommerce\Blocks\Installer;
use Automattic\WooCommerce\Blocks\Migration;
@@ -35,6 +37,7 @@ use Automattic\WooCommerce\StoreApi\StoreApi;
use Automattic\WooCommerce\Blocks\Shipping\ShippingController;
use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplateCompatibility;
use Automattic\WooCommerce\Blocks\Templates\ArchiveProductTemplatesCompatibility;
use Automattic\WooCommerce\Blocks\Domain\Services\OnboardingTasks\TasksController;
/**
* Takes care of bootstrapping the plugin.
@@ -99,6 +102,7 @@ class Bootstrap {
protected function init() {
$this->register_dependencies();
$this->register_payment_methods();
$this->load_interactivity_api();
// This is just a temporary solution to make sure the migrations are run. We have to refactor this. More details: https://github.com/woocommerce/woocommerce-blocks/issues/10196.
if ( $this->package->get_version() !== $this->package->get_version_stored_on_db() ) {
@@ -118,33 +122,45 @@ class Bootstrap {
);
$is_rest = wc()->is_rest_api_request();
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$is_store_api_request = $is_rest && ! empty( $_SERVER['REQUEST_URI'] ) && ( false !== strpos( $_SERVER['REQUEST_URI'], trailingslashit( rest_get_url_prefix() ) . 'wc/store/' ) );
// Load and init assets.
$this->container->get( StoreApi::class )->init();
$this->container->get( PaymentsApi::class )->init();
$this->container->get( DraftOrders::class )->init();
$this->container->get( CreateAccount::class )->init();
$this->container->get( ShippingController::class )->init();
$this->container->get( TasksController::class )->init();
$this->container->get( JetpackWooCommerceAnalytics::class )->init();
// Load assets in admin and on the frontend.
if ( ! $is_rest ) {
$this->add_build_notice();
$this->container->get( AssetDataRegistry::class );
$this->container->get( Installer::class );
$this->container->get( AssetsController::class );
$this->container->get( Installer::class )->init();
$this->container->get( GoogleAnalytics::class )->init();
}
// Load assets unless this is a request specifically for the store API.
if ( ! $is_store_api_request ) {
// Template related functionality. These won't be loaded for store API requests, but may be loaded for
// regular rest requests to maintain compatibility with the store editor.
$this->container->get( BlockPatterns::class );
$this->container->get( BlockTypesController::class );
$this->container->get( BlockTemplatesController::class );
$this->container->get( ProductSearchResultsTemplate::class );
$this->container->get( ProductAttributeTemplate::class );
$this->container->get( CartTemplate::class );
$this->container->get( CheckoutTemplate::class );
$this->container->get( CheckoutHeaderTemplate::class );
$this->container->get( OrderConfirmationTemplate::class );
$this->container->get( ClassicTemplatesCompatibility::class );
$this->container->get( ArchiveProductTemplatesCompatibility::class )->init();
$this->container->get( SingleProductTemplateCompatibility::class )->init();
$this->container->get( Notices::class )->init();
}
$this->container->get( DraftOrders::class )->init();
$this->container->get( CreateAccount::class )->init();
$this->container->get( Notices::class )->init();
$this->container->get( StoreApi::class )->init();
$this->container->get( GoogleAnalytics::class );
$this->container->get( BlockTypesController::class );
$this->container->get( BlockTemplatesController::class );
$this->container->get( ProductSearchResultsTemplate::class );
$this->container->get( ProductAttributeTemplate::class );
$this->container->get( CartTemplate::class );
$this->container->get( CheckoutTemplate::class );
$this->container->get( CheckoutHeaderTemplate::class );
$this->container->get( OrderConfirmationTemplate::class );
$this->container->get( ClassicTemplatesCompatibility::class );
$this->container->get( ArchiveProductTemplatesCompatibility::class )->init();
$this->container->get( SingleProductTemplateCompatibility::class )->init();
$this->container->get( BlockPatterns::class );
$this->container->get( PaymentsApi::class );
$this->container->get( ShippingController::class )->init();
}
/**
@@ -214,6 +230,13 @@ class Bootstrap {
);
}
/**
* Load and set up the Interactivity API if enabled.
*/
protected function load_interactivity_api() {
require_once __DIR__ . '/../Interactivity/load.php';
}
/**
* Register core dependencies with the container.
*/
@@ -339,20 +362,31 @@ class Bootstrap {
$this->container->register(
GoogleAnalytics::class,
function( Container $container ) {
// Require Google Analytics Integration to be activated.
if ( ! class_exists( 'WC_Google_Analytics_Integration', false ) ) {
return;
}
$asset_api = $container->get( AssetApi::class );
return new GoogleAnalytics( $asset_api );
}
);
$this->container->register(
JetpackWooCommerceAnalytics::class,
function( Container $container ) {
$asset_api = $container->get( AssetApi::class );
$asset_data_registry = $container->get( AssetDataRegistry::class );
$block_templates_controller = $container->get( BlockTemplatesController::class );
return new JetpackWooCommerceAnalytics( $asset_api, $asset_data_registry, $block_templates_controller );
}
);
$this->container->register(
Notices::class,
function( Container $container ) {
return new Notices( $container->get( Package::class ) );
}
);
$this->container->register(
Hydration::class,
function( Container $container ) {
return new Hydration( $container->get( AssetDataRegistry::class ) );
}
);
$this->container->register(
PaymentsApi::class,
function ( Container $container ) {
@@ -410,6 +444,12 @@ class Bootstrap {
return new ShippingController( $asset_api, $asset_data_registry );
}
);
$this->container->register(
TasksController::class,
function() {
return new TasksController();
}
);
}
/**

View File

@@ -164,4 +164,18 @@ class FeatureGating {
return self::EXPERIMENTAL_FLAG;
}
/**
* Check if the block templates controller refactor should be used to display blocks.
*
* @return boolean
*/
public function is_block_templates_controller_refactor_enabled() {
if ( file_exists( __DIR__ . '/../../../blocks.ini' ) ) {
$conf = parse_ini_file( __DIR__ . '/../../../blocks.ini' );
return $this->is_development_environment() && isset( $conf['use_block_templates_controller_refactor'] ) && true === (bool) $conf['use_block_templates_controller_refactor'];
}
return false;
}
}

View File

@@ -22,13 +22,16 @@ class GoogleAnalytics {
*/
public function __construct( AssetApi $asset_api ) {
$this->asset_api = $asset_api;
$this->init();
}
/**
* Hook into WP.
*/
protected function init() {
public function init() {
// Require Google Analytics Integration to be activated.
if ( ! class_exists( 'WC_Google_Analytics_Integration', false ) ) {
return;
}
add_action( 'init', array( $this, 'register_assets' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_filter( 'script_loader_tag', array( $this, 'async_script_loader_tags' ), 10, 3 );

View File

@@ -0,0 +1,97 @@
<?php
namespace Automattic\WooCommerce\Blocks\Domain\Services;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
/**
* Service class that handles hydration of API data for blocks.
*/
class Hydration {
/**
* Instance of the asset data registry.
*
* @var AssetDataRegistry
*/
protected $asset_data_registry;
/**
* Cached notices to restore after hydrating the API.
*
* @var array
*/
protected $cached_store_notices = [];
/**
* Constructor.
*
* @param AssetDataRegistry $asset_data_registry Instance of the asset data registry.
*/
public function __construct( AssetDataRegistry $asset_data_registry ) {
$this->asset_data_registry = $asset_data_registry;
}
/**
* Hydrates the asset data registry with data from the API. Disables notices and nonces so requests contain valid
* data that is not polluted by the current session.
*
* @param array $path API paths to hydrate e.g. '/wc/store/v1/cart'.
* @return array Response data.
*/
public function get_rest_api_response_data( $path = '' ) {
$this->cache_store_notices();
$this->disable_nonce_check();
// Preload the request and add it to the array. It will be $preloaded_requests['path'] and contain 'body' and 'headers'.
$preloaded_requests = rest_preload_api_request( [], $path );
$this->restore_cached_store_notices();
$this->restore_nonce_check();
// Returns just the single preloaded request, or an empty array if it doesn't exist.
return $preloaded_requests[ $path ] ?? [];
}
/**
* Disable the nonce check temporarily.
*/
protected function disable_nonce_check() {
add_filter( 'woocommerce_store_api_disable_nonce_check', [ $this, 'disable_nonce_check_callback' ] );
}
/**
* Callback to disable the nonce check. While we could use `__return_true`, we use a custom named callback so that
* we can remove it later without affecting other filters.
*/
public function disable_nonce_check_callback() {
return true;
}
/**
* Restore the nonce check.
*/
protected function restore_nonce_check() {
remove_filter( 'woocommerce_store_api_disable_nonce_check', [ $this, 'disable_nonce_check_callback' ] );
}
/**
* Cache notices before hydrating the API if the customer has a session.
*/
protected function cache_store_notices() {
if ( ! did_action( 'woocommerce_init' ) || null === WC()->session ) {
return;
}
$this->cached_store_notices = WC()->session->get( 'wc_notices', array() );
WC()->session->set( 'wc_notices', null );
}
/**
* Restore notices into current session from cache.
*/
protected function restore_cached_store_notices() {
if ( ! did_action( 'woocommerce_init' ) || null === WC()->session ) {
return;
}
WC()->session->set( 'wc_notices', $this->cached_store_notices );
$this->cached_store_notices = [];
}
}

View File

@@ -0,0 +1,400 @@
<?php
namespace Automattic\WooCommerce\Blocks\Domain\Services;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
use Automattic\WooCommerce\Blocks\BlockTemplatesController;
use Automattic\WooCommerce\Blocks\Package;
use WC_Tracks;
/**
* Service class to integrate Blocks with the Jetpack WooCommerce Analytics extension,
*/
class JetpackWooCommerceAnalytics {
/**
* Instance of the asset API.
*
* @var AssetApi
*/
protected $asset_api;
/**
* Instance of the asset data registry.
*
* @var AssetDataRegistry
*/
protected $asset_data_registry;
/**
* Instance of the block templates controller.
*
* @var BlockTemplatesController
*/
protected $block_templates_controller;
/**
* Whether the required Jetpack WooCommerce Analytics classes are available.
*
* @var bool
*/
protected $is_compatible;
/**
* Constructor.
*
* @param AssetApi $asset_api Instance of the asset API.
* @param AssetDataRegistry $asset_data_registry Instance of the asset data registry.
* @param BlockTemplatesController $block_templates_controller Instance of the block templates controller.
*/
public function __construct( AssetApi $asset_api, AssetDataRegistry $asset_data_registry, BlockTemplatesController $block_templates_controller ) {
$this->asset_api = $asset_api;
$this->asset_data_registry = $asset_data_registry;
$this->block_templates_controller = $block_templates_controller;
}
/**
* Hook into WP.
*/
public function init() {
add_action( 'init', array( $this, 'check_compatibility' ) );
add_action( 'rest_pre_serve_request', array( $this, 'track_local_pickup' ), 10, 4 );
$is_rest = wc()->is_rest_api_request();
if ( ! $is_rest ) {
add_action( 'init', array( $this, 'init_if_compatible' ), 20 );
}
}
/**
* Gets product categories or varation attributes as a formatted concatenated string
*
* @param object $product WC_Product.
* @return string
*/
public function get_product_categories_concatenated( $product ) {
if ( ! $product instanceof WC_Product ) {
return '';
}
$variation_data = $product->is_type( 'variation' ) ? wc_get_product_variation_attributes( $product->get_id() ) : '';
if ( is_array( $variation_data ) && ! empty( $variation_data ) ) {
$line = wc_get_formatted_variation( $variation_data, true );
} else {
$out = array();
$categories = get_the_terms( $product->get_id(), 'product_cat' );
if ( $categories ) {
foreach ( $categories as $category ) {
$out[] = $category->name;
}
}
$line = implode( '/', $out );
}
return $line;
}
/**
* Gather relevant product information. Taken from Jetpack WooCommerce Analytics Module.
*
* @param \WC_Product $product product.
* @return array
*/
public function get_product_details( $product ) {
return array(
'id' => $product->get_id(),
'name' => $product->get_title(),
'category' => $this->get_product_categories_concatenated( $product ),
'price' => $product->get_price(),
'type' => $product->get_type(),
);
}
/**
* Save the order received page view event properties to the asset data registry. The front end will consume these
* later.
*
* @param int $order_id The order ID.
*
* @return void
*/
public function output_order_received_page_view_properties( $order_id ) {
$order = wc_get_order( $order_id );
$product_data = wp_json_encode(
array_map(
function( $item ) {
$product = wc_get_product( $item->get_product_id() );
$product_details = $this->get_product_details( $product );
return array(
'pi' => $product_details['id'],
'pq' => $item->get_quantity(),
'pt' => $product_details['type'],
'pn' => $product_details['name'],
'pc' => $product_details['category'],
'pp' => $product_details['price'],
);
},
$order->get_items()
)
);
$properties = $this->get_cart_checkout_info();
$properties['products'] = $product_data;
$this->asset_data_registry->add( 'wc-blocks-jetpack-woocommerce-analytics_order_received_properties', $properties );
}
/**
* Check compatibility with Jetpack WooCommerce Analytics.
*
* @return void
*/
public function check_compatibility() {
// Require Jetpack WooCommerce Analytics to be available.
$this->is_compatible = class_exists( 'Jetpack_WooCommerce_Analytics_Universal', false ) &&
class_exists( 'Jetpack_WooCommerce_Analytics', false ) &&
\Jetpack_WooCommerce_Analytics::should_track_store();
}
/**
* Initialize if compatible.
*/
public function init_if_compatible() {
if ( ! $this->is_compatible ) {
return;
}
$this->register_assets();
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'register_script_data' ) );
add_action( 'woocommerce_thankyou', array( $this, 'output_order_received_page_view_properties' ) );
}
/**
* Register scripts.
*/
public function register_assets() {
if ( ! $this->is_compatible ) {
return;
}
$asset_file = include Package::get_path() . 'build/wc-blocks-jetpack-woocommerce-analytics.asset.php';
if ( is_array( $asset_file['dependencies'] ) ) {
$this->asset_api->register_script( 'wc-blocks-jetpack-woocommerce-analytics', 'build/wc-blocks-jetpack-woocommerce-analytics.js', array_merge( array( 'wc-blocks' ), $asset_file['dependencies'] ) );
}
}
/**
* Enqueue the Google Tag Manager script if prerequisites are met.
*/
public function enqueue_scripts() {
// Additional check here before finally enqueueing the scripts. Done late here because checking these earlier fails.
if ( ! is_cart() && ! is_checkout() ) {
return;
}
wp_enqueue_script( 'wc-blocks-jetpack-woocommerce-analytics' );
}
/**
* Enqueue the Google Tag Manager script if prerequisites are met.
*/
public function register_script_data() {
$this->asset_data_registry->add( 'wc-blocks-jetpack-woocommerce-analytics_cart_checkout_info', $this->get_cart_checkout_info() );
}
/**
* Get the current user id
*
* @return int
*/
private function get_user_id() {
if ( is_user_logged_in() ) {
$blogid = \Jetpack::get_option( 'id' );
$userid = get_current_user_id();
return $blogid . ':' . $userid;
}
return 'null';
}
/**
* Default event properties which should be included with all events.
*
* @return array Array of standard event props.
*/
public function get_common_properties() {
if ( ! class_exists( 'Jetpack' ) || ! is_callable( array( 'Jetpack', 'get_option' ) ) ) {
return array();
}
return array(
'blog_id' => \Jetpack::get_option( 'id' ),
'ui' => $this->get_user_id(),
'url' => home_url(),
'woo_version' => WC()->version,
);
}
/**
* Get info about the cart & checkout pages, in particular whether the store is using shortcodes or Gutenberg blocks.
* This info is cached in a transient.
*
* @return array
*/
public function get_cart_checkout_info() {
$transient_name = 'woocommerce_blocks_jetpack_woocommerce_analytics_cart_checkout_info_cache';
$info = get_transient( $transient_name );
// Return cached data early to prevent additional processing, the transient lasts for 1 day.
if ( false !== $info ) {
return $info;
}
$cart_template = null;
$checkout_template = null;
$cart_template_id = null;
$checkout_template_id = null;
$templates = $this->block_templates_controller->get_block_templates( array( 'cart', 'checkout', 'page-checkout', 'page-cart' ) );
$guest_checkout = ucfirst( get_option( 'woocommerce_enable_guest_checkout', 'No' ) );
$create_account = ucfirst( get_option( 'woocommerce_enable_signup_and_login_from_checkout', 'No' ) );
foreach ( $templates as $template ) {
if ( 'cart' === $template->slug || 'page-cart' === $template->slug ) {
$cart_template_id = ( $template->id );
continue;
}
if ( 'checkout' === $template->slug || 'page-checkout' === $template->slug ) {
$checkout_template_id = ( $template->id );
}
}
// Get the template and its contents from the IDs we found above.
if ( function_exists( 'get_block_template' ) ) {
$cart_template = get_block_template( $cart_template_id );
$checkout_template = get_block_template( $checkout_template_id );
}
if ( function_exists( 'gutenberg_get_block_template' ) ) {
$cart_template = get_block_template( $cart_template_id );
$checkout_template = get_block_template( $checkout_template_id );
}
// Something failed with the template retrieval, return early with 0 values rather than let a warning appear.
if ( ! $cart_template || ! $checkout_template ) {
return array(
'cart_page_contains_cart_block' => 0,
'cart_page_contains_cart_shortcode' => 0,
'checkout_page_contains_checkout_block' => 0,
'checkout_page_contains_checkout_shortcode' => 0,
);
}
// Update the info transient with data we got from the templates, if the site isn't using WC Blocks we
// won't be doing this so no concern about overwriting.
// Sites that load this code will be loading it on a page using the relevant block, but we still need to check
// the other page to see if it's using the block or shortcode.
$info = array(
'cart_page_contains_cart_block' => str_contains( $cart_template->content, '<!-- wp:woocommerce/cart' ),
'cart_page_contains_cart_shortcode' => str_contains( $cart_template->content, '[woocommerce_cart]' ),
'checkout_page_contains_checkout_block' => str_contains( $checkout_template->content, '<!-- wp:woocommerce/checkout' ),
'checkout_page_contains_checkout_shortcode' => str_contains( $checkout_template->content, '[woocommerce_checkout]' ),
'additional_blocks_on_cart_page' => $this->get_additional_blocks(
$cart_template->content,
array( 'woocommerce/cart' )
),
'additional_blocks_on_checkout_page' => $this->get_additional_blocks(
$checkout_template->content,
array( 'woocommerce/checkout' )
),
'device' => wp_is_mobile() ? 'mobile' : 'desktop',
'guest_checkout' => 'Yes' === $guest_checkout ? 'Yes' : 'No',
'create_account' => 'Yes' === $create_account ? 'Yes' : 'No',
'store_currency' => get_woocommerce_currency(),
);
set_transient( $transient_name, $info, DAY_IN_SECONDS );
return array_merge( $this->get_common_properties(), $info );
}
/**
* Get the additional blocks used in a post or template.
*
* @param string $content The post content.
* @param array $exclude The blocks to exclude.
*
* @return array The additional blocks.
*/
private function get_additional_blocks( $content, $exclude = array() ) {
$parsed_blocks = parse_blocks( $content );
return $this->get_nested_blocks( $parsed_blocks, $exclude );
}
/**
* Get the nested blocks from a block array.
*
* @param array $blocks The blocks array to find nested blocks inside.
* @param string[] $exclude Blocks to exclude, won't find nested blocks within any of the supplied blocks.
*
* @return array
*/
private function get_nested_blocks( $blocks, $exclude = array() ) {
if ( ! is_array( $blocks ) ) {
return array();
}
$additional_blocks = array();
foreach ( $blocks as $block ) {
if ( ! isset( $block['blockName'] ) ) {
continue;
}
if ( in_array( $block['blockName'], $exclude, true ) ) {
continue;
}
if ( is_array( $block['innerBlocks'] ) ) {
$additional_blocks = array_merge( $additional_blocks, self::get_nested_blocks( $block['innerBlocks'], $exclude ) );
}
$additional_blocks[] = $block['blockName'];
}
return $additional_blocks;
}
/**
* Track local pickup settings changes via Store API
*
* @param bool $served Whether the request has already been served.
* @param \WP_REST_Response $result The response object.
* @param \WP_REST_Request $request The request object.
* @return bool
*/
public function track_local_pickup( $served, $result, $request ) {
if ( '/wp/v2/settings' !== $request->get_route() ) {
return $served;
}
// Param name here comes from the show_in_rest['name'] value when registering the setting.
if ( ! $request->get_param( 'pickup_location_settings' ) && ! $request->get_param( 'pickup_locations' ) ) {
return $served;
}
if ( ! $this->is_compatible ) {
return $served;
}
$event_name = 'local_pickup_save_changes';
$settings = $request->get_param( 'pickup_location_settings' );
$locations = $request->get_param( 'pickup_locations' );
$data = array(
'local_pickup_enabled' => 'yes' === $settings['enabled'] ? true : false,
'title' => __( 'Local Pickup', 'woocommerce' ) === $settings['title'],
'price' => '' === $settings['cost'] ? true : false,
'cost' => '' === $settings['cost'] ? 0 : $settings['cost'],
'taxes' => $settings['tax_status'],
'total_pickup_locations' => count( $locations ),
'pickup_locations_enabled' => count(
array_filter(
$locations,
function( $location ) {
return $location['enabled']; }
)
),
);
WC_Tracks::record_event( $event_name, $data );
return $served;
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Automattic\WooCommerce\Blocks\Domain\Services\OnboardingTasks;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
/**
* Review the cart/checkout Task
*/
class ReviewCheckoutTask extends Task {
/**
* ID.
*
* @return string
*/
public function get_id() {
return 'review-checkout-experience';
}
/**
* Title.
*
* @return string
*/
public function get_title() {
return __( 'Review your shopper\'s checkout experience', 'woocommerce' );
}
/**
* Content.
*
* @return string
*/
public function get_content() {
return '';
}
/**
* Time.
*
* @return string
*/
public function get_time() {
return '';
}
/**
* Additional Info.
*
* @return string
*/
public function get_additional_info() {
return __( 'Make sure cart and checkout flows are configured correctly for your shoppers.', 'woocommerce' );
}
/**
* Task completion.
*
* @return bool
*/
public function is_complete() {
return $this->is_visited();
}
/**
* Check if the store uses blocks on the cart or checkout page.
*
* @return boolean
*/
private function has_cart_block() {
$cart_page_id = wc_get_page_id( 'cart' );
$has_block_cart = $cart_page_id && ( has_block( 'woocommerce/cart', $cart_page_id ) || has_block( 'woocommerce/classic-shortcode', $cart_page_id ) );
return $has_block_cart;
}
/**
* Check if the store uses blocks on the cart or checkout page.
*
* @return boolean
*/
private function has_checkout_block() {
$cart_page_id = wc_get_page_id( 'cart' );
$has_block_cart = $cart_page_id && ( has_block( 'woocommerce/cart', $cart_page_id ) || has_block( 'woocommerce/classic-shortcode', $cart_page_id ) );
return $has_block_cart;
}
/**
* Check if the store uses blocks on the cart or checkout page.
*
* @return boolean
*/
private function has_cart_or_checkout_block() {
return $this->has_cart_block() || $this->has_checkout_block();
}
/**
* Task visibility.
*
* @return bool
*/
public function can_view() {
return $this->has_cart_or_checkout_block();
}
/**
* Action URL.
*
* @return string
*/
public function get_action_url() {
$base_url = wc_current_theme_is_fse_theme() ? 'site-editor.php?postType=page&postId=' : 'post.php?action=edit&post=';
$page_id = $this->has_cart_block() ? wc_get_page_id( 'cart' ) : wc_get_page_id( 'checkout' );
$focus = $this->has_cart_block() ? 'cart' : 'checkout';
return admin_url( $base_url . absint( $page_id ) . '&focus=' . $focus . '&canvas=edit' );
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Automattic\WooCommerce\Blocks\Domain\Services\OnboardingTasks;
use Automattic\WooCommerce\Blocks\Domain\Services\OnboardingTasks\ReviewCheckoutTask;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
/**
* Onboarding Tasks Controller
*/
class TasksController {
/**
* Init tasks.
*/
public function init() {
add_action( 'init', [ $this, 'register_tasks' ] );
}
/**
* Register tasks.
*/
public function register_tasks() {
TaskLists::add_task(
'extended',
new ReviewCheckoutTask()
);
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Automattic\WooCommerce\Blocks\Images;
use Automattic\WooCommerce\Blocks\AI\Connection;
/**
* Pexels API client.
*
* @internal
*/
class Pexels {
/**
* The Pexels API endpoint.
*/
const EXTERNAL_MEDIA_PEXELS_ENDPOINT = '/wpcom/v2/external-media/list/pexels';
/**
* Returns the list of images for the given search criteria.
*
* @param Connection $ai_connection The AI connection.
* @param string $token The JWT token.
* @param string $business_description The business description.
*
* @return array|\WP_Error Array of images, or WP_Error if the request failed.
*/
public function get_images( $ai_connection, $token, $business_description ) {
$search_term = $this->define_search_term( $ai_connection, $token, $business_description );
if ( is_wp_error( $search_term ) ) {
return $search_term;
}
return $this->request( $search_term );
}
/**
* Define the search term to be used on Pexels using the AI endpoint.
*
* The search term is a shorter description of the business.
*
* @param Connection $ai_connection The AI connection.
* @param string $token The JWT token.
* @param string $business_description The business description.
*
* @return mixed|\WP_Error
*/
private function define_search_term( $ai_connection, $token, $business_description ) {
$prompt = sprintf( 'Based on the description "%s", provide a one-word product description for the store\'s item. Do not include any adjectives or descriptions of the qualities of the product. The returned word should be simple.', $business_description );
$response = $ai_connection->fetch_ai_response( $token, $prompt );
if ( is_wp_error( $response ) || ! isset( $response['completion'] ) ) {
return new \WP_Error( 'search_term_definition_failed', __( 'The search term definition failed.', 'woocommerce' ) );
}
return $response['completion'];
}
/**
* Make a request to the Pexels API.
*
* @param string $search_term The search term to use.
* @param int $per_page The number of images to return.
*
* @return array|\WP_Error The response body, or WP_Error if the request failed.
*/
private function request( string $search_term, int $per_page = 90 ) {
$request = new \WP_REST_Request( 'GET', self::EXTERNAL_MEDIA_PEXELS_ENDPOINT );
$request->set_param( 'search', esc_html( $search_term ) );
$request->set_param( 'number', $per_page );
$response = rest_do_request( $request );
$response_data = $response->get_data();
if ( $response->is_error() ) {
$error_msg = [
'code' => $response->get_status(),
'data' => $response_data,
];
return new \WP_Error( 'pexels_api_error', __( 'Request to the Pexels API failed.', 'woocommerce' ), $error_msg );
}
$response = $response_data['media'] ?? $response_data;
if ( is_array( $response ) ) {
shuffle( $response );
return $response;
}
return array();
}
}

View File

@@ -8,12 +8,12 @@ namespace Automattic\WooCommerce\Blocks;
* @internal
*/
class Installer {
/**
* Constructor
* Initialize class features.
*/
public function __construct() {
$this->init();
public function init() {
add_action( 'admin_init', array( $this, 'install' ) );
add_filter( 'woocommerce_create_pages', array( $this, 'create_pages' ) );
}
/**
@@ -24,10 +24,24 @@ class Installer {
}
/**
* Initialize class features.
* Modifies default page content replacing it with classic shortcode block.
* We check for shortcode as default because after WooCommerce 8.3, block based checkout is used by default.
* This only runs on Tools > Create Pages as the filter is not applied on WooCommerce plugin activation.
*
* @param array $pages Default pages.
* @return array
*/
protected function init() {
add_action( 'admin_init', array( $this, 'install' ) );
public function create_pages( $pages ) {
if ( '<!-- wp:shortcode -->[woocommerce_cart]<!-- /wp:shortcode -->' === $pages['cart']['content'] ) {
$pages['cart']['content'] = '<!-- wp:woocommerce/classic-shortcode {"shortcode":"cart"} /-->';
}
if ( '<!-- wp:shortcode -->[woocommerce_checkout]<!-- /wp:shortcode -->' === $pages['checkout']['content'] ) {
$pages['checkout']['content'] = '<!-- wp:woocommerce/classic-shortcode {"shortcode":"checkout"} /-->';
}
return $pages;
}
/**

View File

@@ -2,4 +2,3 @@
require __DIR__ . '/class-wc-interactivity-store.php';
require __DIR__ . '/store.php';
require __DIR__ . '/scripts.php';
require __DIR__ . '/client-side-navigation.php';

View File

@@ -1,6 +1,8 @@
<?php
namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
* Takes care of the migrations.
*
@@ -19,6 +21,10 @@ class Migration {
'10.3.0' => array(
'wc_blocks_update_1030_blockified_product_grid_block',
),
'11.2.0' => array(
'wc_blocks_update_1120_rename_checkout_template',
'wc_blocks_update_1120_rename_cart_template',
),
);
/**
@@ -59,4 +65,44 @@ class Migration {
public static function wc_blocks_update_1030_blockified_product_grid_block() {
update_option( Options::WC_BLOCK_USE_BLOCKIFIED_PRODUCT_GRID_BLOCK_AS_TEMPLATE, wc_bool_to_string( false ) );
}
/**
* Rename `checkout` template to `page-checkout`.
*/
public static function wc_blocks_update_1120_rename_checkout_template() {
$template = BlockTemplateUtils::get_block_template( BlockTemplateUtils::PLUGIN_SLUG . '//checkout', 'wp_template' );
if ( $template && ! empty( $template->wp_id ) ) {
if ( ! defined( 'WP_POST_REVISIONS' ) ) {
// This prevents a fatal error when ran outside of admin context.
define( 'WP_POST_REVISIONS', false );
}
wp_update_post(
array(
'ID' => $template->wp_id,
'post_name' => 'page-checkout',
)
);
}
}
/**
* Rename `cart` template to `page-cart`.
*/
public static function wc_blocks_update_1120_rename_cart_template() {
$template = BlockTemplateUtils::get_block_template( BlockTemplateUtils::PLUGIN_SLUG . '//cart', 'wp_template' );
if ( $template && ! empty( $template->wp_id ) ) {
if ( ! defined( 'WP_POST_REVISIONS' ) ) {
// This prevents a fatal error when ran outside of admin context.
define( 'WP_POST_REVISIONS', false );
}
wp_update_post(
array(
'ID' => $template->wp_id,
'post_name' => 'page-cart',
)
);
}
}
}

View File

@@ -109,7 +109,7 @@ class Package {
NewPackage::class,
function ( $container ) {
// leave for automated version bumping.
$version = '10.6.5';
$version = '11.4.8';
return new NewPackage(
$version,
dirname( __DIR__ ),

View File

@@ -0,0 +1,227 @@
<?php
namespace Automattic\WooCommerce\Blocks\Patterns;
use Automattic\WooCommerce\Blocks\AI\Connection;
use WP_Error;
/**
* Pattern Images class.
*/
class PatternUpdater {
/**
* The patterns content option name.
*/
const WC_BLOCKS_PATTERNS_CONTENT = 'wc_blocks_patterns_content';
/**
* Creates the patterns content for the given vertical.
*
* @param Connection $ai_connection The AI connection.
* @param string|WP_Error $token The JWT token.
* @param array|WP_Error $images The array of images.
* @param string $business_description The business description.
*
* @return bool|WP_Error
*/
public function generate_content( $ai_connection, $token, $images, $business_description ) {
if ( is_wp_error( $images ) ) {
return $images;
}
$patterns_with_images = $this->get_patterns_with_images( $images );
if ( is_wp_error( $patterns_with_images ) ) {
return new WP_Error( 'failed_to_set_pattern_images', __( 'Failed to set the pattern images.', 'woocommerce' ) );
}
$patterns_with_images_and_content = $this->get_patterns_with_content( $ai_connection, $token, $patterns_with_images, $business_description );
if ( is_wp_error( $patterns_with_images_and_content ) ) {
return new WP_Error( 'failed_to_set_pattern_content', __( 'Failed to set the pattern content.', 'woocommerce' ) );
}
if ( get_option( self::WC_BLOCKS_PATTERNS_CONTENT ) === $patterns_with_images_and_content ) {
return true;
}
$updated_content = update_option( self::WC_BLOCKS_PATTERNS_CONTENT, $patterns_with_images_and_content );
if ( ! $updated_content ) {
return new WP_Error( 'failed_to_update_patterns_content', __( 'Failed to update patterns content.', 'woocommerce' ) );
}
return $updated_content;
}
/**
* Returns the patterns with images.
*
* @param array $selected_images The array of images.
*
* @return array|WP_Error The patterns with images.
*/
private function get_patterns_with_images( $selected_images ) {
$patterns_dictionary = $this->get_patterns_dictionary();
if ( is_wp_error( $patterns_dictionary ) ) {
return $patterns_dictionary;
}
$patterns_with_images = array();
foreach ( $patterns_dictionary as $pattern ) {
if ( ! $this->pattern_has_images( $pattern ) ) {
$patterns_with_images[] = $pattern;
continue;
}
list( $images, $alts ) = $this->get_images_for_pattern( $pattern, $selected_images );
if ( empty( $images ) ) {
$patterns_with_images[] = $pattern;
continue;
}
$pattern['images'] = $images;
$string = wp_json_encode( $pattern );
foreach ( $alts as $i => $alt ) {
$alt = empty( $alt ) ? 'the text should be related to the store description but generic enough to adapt to any image' : $alt;
$string = str_replace( "{image.$i}", $alt, $string );
}
$pattern = json_decode( $string, true );
$patterns_with_images[] = $pattern;
}
return $patterns_with_images;
}
/**
* Returns the patterns with AI generated content.
*
* @param Connection $ai_connection The AI connection.
* @param string|WP_Error $token The JWT token.
* @param array $patterns The array of patterns.
* @param string $business_description The business description.
*
* @return array|WP_Error The patterns with AI generated content.
*/
private function get_patterns_with_content( $ai_connection, $token, $patterns, $business_description ) {
if ( is_wp_error( $token ) ) {
return $token;
}
$patterns_with_content = $patterns;
$prompts = array();
foreach ( $patterns_with_content as $pattern ) {
$prompt = sprintf( 'Given the following store description: "%s", and the following JSON file representing the content of the "%s" pattern: %s.\n', $business_description, $pattern['name'], wp_json_encode( $pattern['content'] ) );
$prompt .= "Replace the titles, descriptions and button texts in each 'default' key using the prompt in the corresponding 'ai_prompt' key by a text that is related to the previous store description (but not the exact text) and matches the 'ai_prompt', the length of each replacement should be similar to the 'default' text length. The text should not be written in first-person. The response should be only a JSON string, with absolutely no intro or explanations.";
$prompts[] = $prompt;
}
$responses = $ai_connection->fetch_ai_responses( $token, $prompts );
foreach ( $responses as $key => $response ) {
// If the AI response is invalid, we skip the pattern and keep the default content.
if ( is_wp_error( $response ) || empty( $response ) ) {
continue;
}
if ( ! isset( $response['completion'] ) ) {
continue;
}
$pattern_content = json_decode( $response['completion'], true );
if ( ! is_null( $pattern_content ) ) {
$patterns_with_content[ $key ]['content'] = $pattern_content;
}
}
return $patterns_with_content;
}
/**
* Get the Patterns Dictionary.
*
* @return mixed|WP_Error|null
*/
private function get_patterns_dictionary() {
$patterns_dictionary = plugin_dir_path( __FILE__ ) . 'dictionary.json';
if ( ! file_exists( $patterns_dictionary ) ) {
return new WP_Error( 'missing_patterns_dictionary', __( 'The patterns dictionary is missing.', 'woocommerce' ) );
}
return wp_json_file_decode( $patterns_dictionary, array( 'associative' => true ) );
}
/**
* Returns whether the pattern has images.
*
* @param array $pattern The array representing the pattern.
*
* @return bool True if the pattern has images, false otherwise.
*/
private function pattern_has_images( array $pattern ): bool {
return isset( $pattern['images_total'] ) && $pattern['images_total'] > 0;
}
/**
* Returns the images for the given pattern.
*
* @param array $pattern The array representing the pattern.
* @param array $selected_images The array of images.
*
* @return array An array containing an array of the images in the first position and their alts in the second.
*/
private function get_images_for_pattern( array $pattern, array $selected_images ): array {
$images = array();
$alts = array();
foreach ( $selected_images as $selected_image ) {
if ( ! isset( $selected_image['title'] ) ) {
continue;
}
if ( ! isset( $selected_image['URL'] ) ) {
continue;
}
if ( str_contains( '.jpeg', $selected_image['title'] ) ) {
continue;
}
$expected_image_format = $pattern['images_format'] ?? 'portrait';
$selected_image_format = $this->get_selected_image_format( $selected_image );
if ( $selected_image_format !== $expected_image_format ) {
continue;
}
$images[] = $selected_image['URL'];
$alts[] = $selected_image['title'];
}
return array( $images, $alts );
}
/**
* Returns the selected image format. Defaults to landscape.
*
* @param array $selected_image The selected image to be assigned to the pattern.
*
* @return string The selected image format.
*/
private function get_selected_image_format( $selected_image ) {
if ( ! isset( $selected_image['width'], $selected_image['height'] ) ) {
return 'portrait';
}
return $selected_image['width'] === $selected_image['height'] ? 'square' : ( $selected_image['width'] > $selected_image['height'] ? 'landscape' : 'portrait' );
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Automattic\WooCommerce\Blocks\Patterns;
use WP_Error;
/**
* Pattern Images Helper class.
*/
class PatternsHelper {
/**
* Returns the pattern content.
*
* @param string $pattern_slug The pattern slug.
*
* @return array The pattern content.
*/
public static function get_pattern_content( string $pattern_slug ) {
$pattern = self::get_patterns_dictionary( $pattern_slug );
if ( empty( $pattern ) ) {
return array();
}
if ( ! isset( $pattern['content'] ) ) {
return array();
}
return $pattern['content'];
}
/**
* Returns the pattern images.
*
* @param string $pattern_slug The pattern slug.
*
* @return array The pattern images.
*/
public static function get_pattern_images( string $pattern_slug ): array {
$pattern = self::get_patterns_dictionary( $pattern_slug );
if ( empty( $pattern ) ) {
return array();
}
if ( ! isset( $pattern['images'] ) ) {
return array();
}
if ( ! isset( $pattern['images_total'] ) ) {
return array();
}
return $pattern['images'];
}
/**
* Returns the image for the given pattern.
*
* @param array $images The array of images.
* @param int $index The index of the image to return.
* @param string $default_image The default image to return.
*
* @return string The image.
*/
public static function get_image_url( array $images, int $index, string $default_image ): string {
$image = filter_var( $default_image, FILTER_VALIDATE_URL )
? $default_image
: plugins_url( $default_image, dirname( __DIR__ ) );
if ( isset( $images[ $index ] ) ) {
$image = $images[ $index ];
}
return $image;
}
/**
* Get the Patterns Dictionary.
*
* @param string|null $pattern_slug The pattern slug.
*
* @return mixed|WP_Error|null
*/
private static function get_patterns_dictionary( $pattern_slug = null ) {
$patterns_dictionary = get_option( PatternUpdater::WC_BLOCKS_PATTERNS_CONTENT );
if ( ! empty( $patterns_dictionary ) ) {
if ( empty( $pattern_slug ) ) {
return $patterns_dictionary;
}
foreach ( $patterns_dictionary as $pattern_dictionary ) {
if ( $pattern_dictionary['slug'] === $pattern_slug ) {
return $pattern_dictionary;
}
}
}
$patterns_dictionary_file = plugin_dir_path( __FILE__ ) . 'dictionary.json';
if ( ! file_exists( $patterns_dictionary_file ) ) {
return new WP_Error( 'missing_patterns_dictionary', __( 'The patterns dictionary is missing.', 'woocommerce' ) );
}
$patterns_dictionary = wp_json_file_decode( $patterns_dictionary_file, array( 'associative' => true ) );
if ( ! empty( $pattern_slug ) ) {
foreach ( $patterns_dictionary as $pattern_dictionary ) {
if ( $pattern_dictionary['slug'] === $pattern_slug ) {
return $pattern_dictionary;
}
}
}
return $patterns_dictionary;
}
}

View File

@@ -0,0 +1,401 @@
<?php
namespace Automattic\WooCommerce\Blocks\Patterns;
use Automattic\WooCommerce\Blocks\AI\Connection;
use WP_Error;
/**
* Pattern Images class.
*/
class ProductUpdater {
/**
* Generate AI content and assign AI-managed images to Products.
*
* @param Connection $ai_connection The AI connection.
* @param string $token The JWT token.
* @param array $images The array of images.
* @param string $business_description The business description.
*
* @return bool|WP_Error True if the content was generated successfully, WP_Error otherwise.
*/
public function generate_content( $ai_connection, $token, $images, $business_description ) {
if ( empty( $business_description ) ) {
return new \WP_Error( 'missing_business_description', __( 'No business description provided for generating AI content.', 'woocommerce' ) );
}
$last_business_description = get_option( 'last_business_description_with_ai_content_generated' );
if ( $last_business_description === $business_description ) {
if ( is_string( $business_description ) && is_string( $last_business_description ) ) {
return true;
} else {
return new \WP_Error( 'business_description_not_found', __( 'No business description provided for generating AI content.', 'woocommerce' ) );
}
}
$real_products = $this->fetch_product_ids();
if ( is_array( $real_products ) && count( $real_products ) > 0 ) {
return true;
}
$dummy_products = $this->fetch_product_ids( 'dummy' );
if ( ! is_array( $dummy_products ) ) {
return new \WP_Error( 'failed_to_fetch_dummy_products', __( 'Failed to fetch dummy products.', 'woocommerce' ) );
}
$dummy_products_count = count( $dummy_products );
$expected_dummy_products_count = 6;
$products_to_create = max( 0, $expected_dummy_products_count - $dummy_products_count );
while ( $products_to_create > 0 ) {
$this->create_new_product();
$products_to_create--;
}
// Identify dummy products that need to have their content updated.
$dummy_products_ids = $this->fetch_product_ids( 'dummy' );
if ( ! is_array( $dummy_products_ids ) ) {
return new \WP_Error( 'failed_to_fetch_dummy_products', __( 'Failed to fetch dummy products.', 'woocommerce' ) );
}
$dummy_products = array_map(
function ( $product ) {
return wc_get_product( $product->ID );
},
$dummy_products_ids
);
$dummy_products_to_update = [];
foreach ( $dummy_products as $dummy_product ) {
if ( ! $dummy_product instanceof \WC_Product ) {
continue;
}
$should_update_dummy_product = $this->should_update_dummy_product( $dummy_product );
if ( $should_update_dummy_product ) {
$dummy_products_to_update[] = $dummy_product;
}
}
if ( empty( $dummy_products_to_update ) ) {
return true;
}
$ai_selected_products_images = $this->get_images_information( $images );
$products_information_list = $this->assign_ai_selected_images_to_dummy_products_information_list( $ai_selected_products_images );
$response = $this->generate_product_content( $ai_connection, $token, $products_information_list );
if ( is_wp_error( $response ) ) {
$error_msg = $response;
} elseif ( empty( $response ) || ! isset( $response['completion'] ) ) {
$error_msg = new \WP_Error( 'missing_completion_key', __( 'The response from the AI service is empty or missing the completion key.', 'woocommerce' ) );
}
if ( isset( $error_msg ) ) {
$this->update_dummy_products( $dummy_products_to_update, $products_information_list );
return $error_msg;
}
$product_content = json_decode( $response['completion'], true );
if ( is_null( $product_content ) ) {
$this->update_dummy_products( $dummy_products_to_update, $products_information_list );
return new \WP_Error( 'invalid_json', __( 'The response from the AI service is not a valid JSON.', 'woocommerce' ) );
}
// This is required to allow the usage of the media_sideload_image function outside the context of /wp-admin/.
// See https://developer.wordpress.org/reference/functions/media_sideload_image/ for more details.
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$this->update_dummy_products( $dummy_products_to_update, $product_content );
return true;
}
/**
* Update the dummy products with the content from the information list.
*
* @param array $dummy_products_to_update The dummy products to update.
* @param array $products_information_list The products information list.
*/
public function update_dummy_products( $dummy_products_to_update, $products_information_list ) {
$i = 0;
foreach ( $dummy_products_to_update as $dummy_product ) {
if ( ! isset( $products_information_list[ $i ] ) ) {
continue;
}
$this->update_product_content( $dummy_product, $products_information_list[ $i ] );
++$i;
}
}
/**
* Verify if the dummy product should have its content generated and managed by AI.
*
* @param \WC_Product $dummy_product The dummy product.
*
* @return bool
*/
public function should_update_dummy_product( $dummy_product ): bool {
$current_product_hash = $this->get_hash_for_product( $dummy_product );
$ai_modified_product_hash = $this->get_hash_for_ai_modified_product( $dummy_product );
$date_created = $dummy_product->get_date_created();
$date_modified = $dummy_product->get_date_modified();
if ( ! $date_created instanceof \WC_DateTime || ! $date_modified instanceof \WC_DateTime ) {
return false;
}
$formatted_date_created = $dummy_product->get_date_created()->date( 'Y-m-d H:i:s' );
$formatted_date_modified = $dummy_product->get_date_modified()->date( 'Y-m-d H:i:s' );
$timestamp_created = strtotime( $formatted_date_created );
$timestamp_modified = strtotime( $formatted_date_modified );
$dummy_product_not_modified = abs( $timestamp_modified - $timestamp_created ) < 60;
if ( $current_product_hash === $ai_modified_product_hash || $dummy_product_not_modified ) {
return true;
}
return false;
}
/**
* Creates a new product and assigns the _headstart_post meta to it.
*
* @return bool|int
*/
public function create_new_product() {
$product = new \WC_Product();
$random_price = wp_rand( 5, 50 );
$product->set_name( 'My Awesome Product' );
$product->set_status( 'publish' );
$product->set_description( 'Product description' );
$product->set_price( $random_price );
$product->set_regular_price( $random_price );
$saved_product = $product->save();
return update_post_meta( $saved_product, '_headstart_post', true );
}
/**
* Return all existing products that have the _headstart_post meta assigned to them.
*
* @param string $type The type of products to fetch.
*
* @return array
*/
public function fetch_product_ids( $type = 'user_created' ) {
global $wpdb;
if ( 'user_created' === $type ) {
return $wpdb->get_results( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE ID NOT IN ( SELECT p.ID FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id WHERE pm.meta_key = %s AND p.post_type = 'product' AND p.post_status = 'publish' ) AND post_type = 'product' AND post_status = 'publish' LIMIT 6", '_headstart_post' ) );
}
return $wpdb->get_results( $wpdb->prepare( "SELECT p.ID FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id WHERE pm.meta_key = %s AND p.post_type = 'product' AND p.post_status = 'publish'", '_headstart_post' ) );
}
/**
* Return the hash for a product based on its name, description and image_id.
*
* @param \WC_Product $product The product.
*
* @return false|string
*/
public function get_hash_for_product( $product ) {
if ( ! $product instanceof \WC_Product ) {
return false;
}
return md5( $product->get_name() . $product->get_description() . $product->get_image_id() );
}
/**
* Return the hash for a product that had its content AI-generated.
*
* @param \WC_Product $product The product.
*
* @return false|mixed
*/
public function get_hash_for_ai_modified_product( $product ) {
if ( ! $product instanceof \WC_Product ) {
return false;
}
return get_post_meta( $product->get_id(), '_ai_generated_content', true );
}
/**
* Create a hash with the AI-generated content and save it as a meta for the product.
*
* @param \WC_Product $product The product.
*
* @return bool|int
*/
public function create_hash_for_ai_modified_product( $product ) {
if ( ! $product instanceof \WC_Product ) {
return false;
}
$content = $this->get_hash_for_product( $product );
return update_post_meta( $product->get_id(), '_ai_generated_content', $content );
}
/**
* Update the product content with the AI-generated content.
*
* @param \WC_Product $product The product.
* @param array $ai_generated_product_content The AI-generated content.
*
* @return string|void
*/
public function update_product_content( $product, $ai_generated_product_content ) {
if ( ! $product instanceof \WC_Product ) {
return;
}
if ( ! isset( $ai_generated_product_content['image']['src'] ) || ! isset( $ai_generated_product_content['image']['alt'] ) || ! isset( $ai_generated_product_content['title'] ) || ! isset( $ai_generated_product_content['description'] ) ) {
return;
}
// Since the media_sideload_image function is expensive and can take longer to complete
// the process of downloading the external image and uploading it to the media library,
// here we are increasing the time limit and the memory limit to avoid any issues.
set_time_limit( 60 );
wp_raise_memory_limit();
$product_image_id = media_sideload_image( $ai_generated_product_content['image']['src'], $product->get_id(), $ai_generated_product_content['image']['alt'], 'id' );
if ( is_wp_error( $product_image_id ) ) {
return $product_image_id->get_error_message();
}
$product->set_name( $ai_generated_product_content['title'] );
$product->set_description( $ai_generated_product_content['description'] );
$product->set_image_id( $product_image_id );
$product->save();
$this->create_hash_for_ai_modified_product( $product );
}
/**
* Assigns the default content for the products.
*
* @param array $ai_selected_products_images The images information.
*
* @return array[]
*/
public function assign_ai_selected_images_to_dummy_products_information_list( $ai_selected_products_images ) {
$default_image = [
'src' => esc_url( plugins_url( 'woocommerce-blocks/images/block-placeholders/product-image-gallery.svg' ) ),
'alt' => 'The placeholder for a product image.',
];
return [
[
'title' => 'A product title',
'description' => 'A product description',
'image' => $ai_selected_products_images[0] ?? $default_image,
],
[
'title' => 'A product title',
'description' => 'A product description',
'image' => $ai_selected_products_images[1] ?? $default_image,
],
[
'title' => 'A product title',
'description' => 'A product description',
'image' => $ai_selected_products_images[2] ?? $default_image,
],
[
'title' => 'A product title',
'description' => 'A product description',
'image' => $ai_selected_products_images[3] ?? $default_image,
],
[
'title' => 'A product title',
'description' => 'A product description',
'image' => $ai_selected_products_images[4] ?? $default_image,
],
[
'title' => 'A product title',
'description' => 'A product description',
'image' => $ai_selected_products_images[5] ?? $default_image,
],
];
}
/**
* Get the images information.
*
* @param array $images The array of images.
*
* @return array
*/
public function get_images_information( $images ) {
if ( is_wp_error( $images ) ) {
return [
'src' => 'images/block-placeholders/product-image-gallery.svg',
'alt' => 'The placeholder for a product image.',
];
}
$count = 0;
$placeholder_images = [];
foreach ( $images as $image ) {
if ( $count >= 6 ) {
break;
}
if ( ! isset( $image['title'] ) || ! isset( $image['thumbnails']['medium'] ) ) {
continue;
}
$placeholder_images[] = [
'src' => esc_url( $image['thumbnails']['medium'] ),
'alt' => esc_attr( $image['title'] ),
];
++ $count;
}
return $placeholder_images;
}
/**
* Generate the product content.
*
* @param Connection $ai_connection The AI connection.
* @param string $token The JWT token.
* @param array $products_default_content The default content for the products.
*
* @return array|int|string|\WP_Error
*/
public function generate_product_content( $ai_connection, $token, $products_default_content ) {
$store_description = get_option( 'woo_ai_describe_store_description' );
if ( ! $store_description ) {
return new \WP_Error( 'missing_store_description', __( 'The store description is required to generate the content for your site.', 'woocommerce' ) );
}
$prompt = sprintf( 'Given the following business description: "%1s" and the assigned value for the alt property in the json bellow, generate new titles and descriptions for each one of the products listed bellow and assign them as the new values for the json: %2s. Each one of the titles should be unique and must be limited to 29 characters. The response should be only a JSON string, with no intro or explanations.', $store_description, wp_json_encode( $products_default_content ) );
return $ai_connection->fetch_ai_response( $token, $prompt, 60 );
}
}

View File

@@ -0,0 +1,656 @@
[
{
"name": "Banner",
"slug": "woocommerce-blocks/banner",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Save up to 60%",
"ai_prompt": "A title advertising the sale"
}
],
"descriptions": [
{
"default": "Holiday Sale",
"ai_prompt": "A label with the sale name"
},
{
"default": "Make the day special with our collection of discounted products.",
"ai_prompt": "The main description of the sale"
}
],
"buttons": [
{
"default": "Shop Holiday Sale",
"ai_prompt": "The button text to go to the sale page"
}
]
}
},
{
"name": "Discount Banner",
"slug": "woocommerce-blocks/discount-banner",
"content": {
"descriptions": [
{
"default": "Select products",
"ai_prompt": "A description of the products on sale"
}
]
}
},
{
"name": "Discount Banner with Image",
"slug": "woocommerce-blocks/discount-banner-with-image",
"images_total": 1,
"images_format": "landscape",
"content": {
"descriptions": [
{
"default": "Select products",
"ai_prompt": "A description of the products on sale"
}
]
}
},
{
"name": "Featured Category Focus",
"slug": "woocommerce-blocks/featured-category-focus",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Announcing our newest collection",
"ai_prompt": "The title of the featured category: {image.0}"
}
]
}
},
{
"name": "Featured Category Triple",
"slug": "woocommerce-blocks/featured-category-triple",
"images_total": 3,
"images_format": "portrait",
"content": {
"titles": [
{
"default": "Cupcakes",
"ai_prompt": "The title of the first featured category: {image.0}"
},
{
"default": "Sweet Danish",
"ai_prompt": "The title of the second featured category: {image.1}"
},
{
"default": "Warm Bread",
"ai_prompt": "The title of the third featured category: {image.2}"
}
]
}
},
{
"name": "Featured Products: Fresh & Tasty",
"slug": "woocommerce-blocks/featured-products-fresh-and-tasty",
"images_total": 4,
"images_format": "portrait",
"content": {
"titles": [
{
"default": "Fresh & tasty goods",
"ai_prompt": "The title of the featured products"
}
],
"descriptions": [
{
"default": "Sweet Organic Lemons",
"ai_prompt": "The description of the first featured products: {image.0}"
},
{
"default": "Fresh Organic Tomatoes",
"ai_prompt": "The description of the second featured products: {image.1}"
},
{
"default": "Fresh Lettuce (Washed)",
"ai_prompt": "The description of the third featured products: {image.2}"
},
{
"default": "Russet Organic Potatoes",
"ai_prompt": "The description of the fourth featured products: {image.3}"
}
]
}
},
{
"name": "Hero Product 3 Split",
"slug": "woocommerce-blocks/hero-product-3-split",
"images_total": 1,
"images_format": "portrait",
"content": {
"titles": [
{
"default": "New in: 3-in-1 parka",
"ai_prompt": "An impact phrase that advertises the displayed product: {image.0}. The title must have less than 30 characters"
},
{
"default": "Waterproof Membrane",
"ai_prompt": "A title describing the first displayed product feature. The title must have only 2 or 3 words."
},
{
"default": "Expert Craftsmanship",
"ai_prompt": "A title describing the second displayed product feature. The title must have only 2 or 3 words."
},
{
"default": "Durable Fabric",
"ai_prompt": "A title describing the third displayed product feature. The title must have only 2 or 3 words."
}
],
"descriptions": [
{
"default": "Never worry about the weather again. Keep yourself dry, warm, and looking stylish.",
"ai_prompt": "A description of the first displayed product feature. The description must have less than 120 characters."
},
{
"default": "Our products are made with expert craftsmanship and attention to detail.",
"ai_prompt": "A description of the second displayed product feature. The description must have less than 120 characters."
},
{
"default": "We use only the highest-quality materials in our products, ensuring that they look great.",
"ai_prompt": "A description of the third displayed product feature. The description must have less than 120 characters."
}
]
}
},
{
"name": "Hero Product Chessboard",
"slug": "woocommerce-blocks/hero-product-chessboard",
"images_total": 2,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "The Fall Collection",
"ai_prompt": "An impact phrase that advertises the displayed product: {image.0}"
},
{
"default": "Quality Materials",
"ai_prompt": "A title describing the first displayed product feature"
},
{
"default": "Expert Craftsmanship",
"ai_prompt": "A title describing the second displayed product feature"
},
{
"default": "Unique Design",
"ai_prompt": "A title describing the third displayed product feature"
},
{
"default": "Customer Satisfaction",
"ai_prompt": "A title describing the fourth displayed product feature"
}
],
"descriptions": [
{
"default": "With high-quality materials and expert craftsmanship, our products are built to last and exceed your expectations.",
"ai_prompt": "A description of the product"
},
{
"default": "We use only the highest-quality materials in our products, ensuring that they look great and last for years to come.",
"ai_prompt": "A description of the first displayed product feature"
},
{
"default": "Our products are made with expert craftsmanship and attention to detail, ensuring that every stitch and seam is perfect.",
"ai_prompt": "A description of the second displayed product feature"
},
{
"default": "From bold prints and colors to intricate details and textures, our products are a perfect combination of style and function.",
"ai_prompt": "A description of the third displayed product feature"
},
{
"default": "Our top priority is customer satisfaction, and we stand behind our products 100%. ",
"ai_prompt": "A description of the fourth displayed product feature"
}
]
}
},
{
"name": "Hero Product Split",
"slug": "woocommerce-blocks/hero-product-split",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Get cozy this fall with knit sweaters",
"ai_prompt": "An impact phrase that advertises the displayed product: {image.0}"
}
]
}
},
{
"name": "Just Arrived Full Hero",
"slug": "woocommerce-blocks/just-arrived-full-hero",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Just arrived",
"ai_prompt": "An impact phrase that advertises the displayed product collection"
}
],
"descriptions": [
{
"default": "Our early autumn collection is here.",
"ai_prompt": "A description of the product collection"
}
],
"buttons": [
{
"default": "Shop now",
"ai_prompt": "The button text to go to the product collection page"
}
]
}
},
{
"name": "Product Collection Banner",
"slug": "woocommerce-blocks/product-collection-banner",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Brand New for the Holidays",
"ai_prompt": "An impact phrase that advertises the displayed product collection: {image.0}"
}
],
"descriptions": [
{
"default": "Check out our brand new collection of holiday products and find the right gift for anyone.",
"ai_prompt": "A description of the product collection"
}
]
}
},
{
"name": "Product Collections Featured Collection",
"slug": "woocommerce-blocks/product-collections-featured-collection",
"content": {
"titles": [
{
"default": "This week's popular products",
"ai_prompt": "An impact phrase that advertises the displayed product collection"
}
]
}
},
{
"name": "Product Collections Featured Collections",
"slug": "woocommerce-blocks/product-collections-featured-collections",
"images_total": 4,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Tech gifts under $100",
"ai_prompt": "An impact phrase that advertises the first product collection: {image.0}, {image.1}"
},
{
"default": "For the gamers",
"ai_prompt": "An impact phrase that advertises the second product collection: {image.2}, {image.3}"
}
],
"buttons": [
{
"default": "Shop tech",
"ai_prompt": "The button text to go to the first product collection page"
},
{
"default": "Shop games",
"ai_prompt": "The button text to go to the second product collection page"
}
]
}
},
{
"name": "Product Collections Newest Arrivals",
"slug": "woocommerce-blocks/product-collections-newest-arrivals",
"content": {
"titles": [
{
"default": "Our newest arrivals",
"ai_prompt": "An impact phrase that advertises the displayed product collection"
}
],
"buttons": [
{
"default": "More new products",
"ai_prompt": "The button text to go to the product collection page"
}
]
}
},
{
"name": "Product Gallery",
"slug": "woocommerce-blocks/product-query-product-gallery",
"content": {
"titles": [
{
"default": "Our newest arrivals",
"ai_prompt": "An impact phrase that advertises the displayed product collection"
}
]
}
},
{
"name": "Product Collection 3 Columns",
"slug": "woocommerce-blocks/product-collection-3-columns",
"content": {
"titles": [
{
"default": "Our newest arrivals",
"ai_prompt": "An impact phrase that advertises the displayed product collection"
}
]
}
},
{
"name": "Product Collection 4 Columns",
"slug": "woocommerce-blocks/product-collection-4-columns",
"content": {
"titles": [
{
"default": "Our newest arrivals",
"ai_prompt": "An impact phrase that advertises the displayed product collection"
}
]
}
},
{
"name": "Product Collection 5 Columns",
"slug": "woocommerce-blocks/product-collection-5-columns",
"content": {
"titles": [
{
"default": "Our newest arrivals",
"ai_prompt": "An impact phrase that advertises the displayed product collection"
}
]
}
},
{
"name": "Featured Products 2 Columns",
"slug": "woocommerce-blocks/featured-products-2-cols",
"content": {
"titles": [
{
"default": "Fan favorites",
"ai_prompt": "An impact phrase that advertises the features products"
}
],
"descriptions": [
{
"default": "Get ready to start the season right. All the fan favorites in one place at the best price.",
"ai_prompt": "A description of the featured products"
}
],
"buttons": [
{
"default": "Shop All",
"ai_prompt": "The button text to go to the featured products page"
}
]
}
},
{
"name": "Product Hero 2 Column 2 Row",
"slug": "woocommerce-blocks/product-hero-2-col-2-row",
"images_total": 2,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "The Eden Jacket",
"ai_prompt": "An title that advertises the displayed product: {image.0}"
},
{
"default": "100% Woolen",
"ai_prompt": "A title that advertises the first product feature"
},
{
"default": "Fits your wardrobe",
"ai_prompt": "A title that advertises the second product feature"
},
{
"default": "Versatile",
"ai_prompt": "A title that advertises the third product feature"
},
{
"default": "Normal Fit",
"ai_prompt": "A title that advertises the fourth product feature"
}
],
"descriptions": [
{
"default": "Perfect for any look featuring a mid-rise, relax fitting silhouette.",
"ai_prompt": "A description of the displayed product: {image.0}"
},
{
"default": "Reflect your fashionable style.",
"ai_prompt": "A description of the first product feature"
},
{
"default": "Half tuck into your pants or layer over.",
"ai_prompt": "A description of the second product feature"
},
{
"default": "Button-down front for any type of mood or look.",
"ai_prompt": "A description of the third product feature"
},
{
"default": "42% Cupro 34% Linen 24% Viscose",
"ai_prompt": "A description of the fourth product feature"
}
],
"buttons": [
{
"default": "View product",
"ai_prompt": "The button text to go to the product page"
}
]
}
},
{
"name": "Shop by Price",
"slug": "woocommerce-blocks/shop-by-price",
"content": {
"titles": [
{
"default": "Outdoor Furniture & Accessories",
"ai_prompt": "An impact phrase that advertises the first product collection"
},
{
"default": "Summer Dinning",
"ai_prompt": "An impact phrase that advertises the second product collection"
},
{
"default": "Women's Styles",
"ai_prompt": "An impact phrase that advertises the third product collection"
},
{
"default": "Kids' Styles",
"ai_prompt": "An impact phrase that advertises the fourth product collection"
}
]
}
},
{
"name": "Small Discount Banner with Image",
"slug": "woocommerce-blocks/small-discount-banner-with-image",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Chairs",
"ai_prompt": "An impact phrase that advertises a products: {image.0}"
}
]
}
},
{
"name": "Social: Follow us on social media",
"slug": "woocommerce-blocks/social-follow-us-in-social-media",
"images_total": 4,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Follow us on social media",
"ai_prompt": "An phrase that advertises the social media accounts"
}
]
}
},
{
"name": "Alternating Image and Text",
"slug": "woocommerce-blocks/alt-image-and-text",
"images_total": 2,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "The goods",
"ai_prompt": "An impact phrase that advertises the products"
},
{
"default": "Created with love and care in Australia",
"ai_prompt": "An impact phrase that advertises the products: {image.0}"
},
{
"default": "About us",
"ai_prompt": "An impact phrase that advertises the brand"
},
{
"default": "Marl is an independent studio and artisanal gallery: {image.1}",
"ai_prompt": "An impact phrase that advertises the brand"
}
],
"descriptions": [
{
"default": "All items are 100% hand-made, using the potters wheel or traditional techniques.\n\nTimeless style.\n\nEarthy, organic feel.\n\nEnduring quality.\n\nUnique, one-of-a-kind pieces.",
"ai_prompt": "A description of the products"
},
{
"default": "We specialize in limited collections of handmade tableware. We collaborate with restaurants and cafes to create unique items that complement the menu perfectly. Please get in touch if you want to know more about our process and pricing.",
"ai_prompt": "A description of the products"
}
],
"buttons": [
{
"default": "Learn more",
"ai_prompt": "The button text to go to the product page"
}
]
}
},
{
"name": "Testimonials 3 Columns",
"slug": "woocommerce-blocks/testimonials-3-columns",
"content": {
"titles": [
{
"default": "What our customers say",
"ai_prompt": "A title that advertises the set of testimonials"
},
{
"default": "Great experience",
"ai_prompt": "A title that advertises the first testimonial"
},
{
"default": "LOVE IT",
"ai_prompt": "A title that advertises the second testimonial"
},
{
"default": "Awesome couch",
"ai_prompt": "A title that advertises the third testimonial"
}
],
"descriptions": [
{
"default": "In the end the couch wasn't exactly what I was looking for but my experience with the Burrow team was excellent. First in providing a discount when the couch was delayed.",
"ai_prompt": "A description of the first testimonial. The testimonial must have less than 200 characters."
},
{
"default": "Great couch. color as advertise. seat is nice and firm. Easy to put together. Versatile. Bought one for my mother in law as well. And she loves hers!",
"ai_prompt": "A description of the second testimonial. The testimonial must have less than 200 characters."
},
{
"default": "I got the kind sofa. The look and feel is high quality, and I enjoy that it is a medium level of firmness. Assembly took a little longer than I expected, and it came in 4 boxes.",
"ai_prompt": "A description of the third testimonial. The testimonial must have less than 200 characters."
}
]
}
},
{
"name": "Testimonials Single",
"slug": "woocommerce-blocks/testimonials-single",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Great experience",
"ai_prompt": "A title that advertises the testimonial"
}
],
"descriptions": [
{
"default": "In the end the couch wasn't exactly what I was looking for but my experience with the Burrow team was excellent. First in providing a discount when the couch was delayed, then timely feedback and updates as the...\n\n~ Anna W.",
"ai_prompt": "A description of the testimonial. The testimonial must have less than 200 characters."
}
]
}
},
{
"name": "Featured Category Cover Image",
"slug": "woocommerce-blocks/featured-category-cover-image",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "100% natural denim",
"ai_prompt": "A description for a product"
}
],
"descriptions": [
{
"default": "Only the finest goes into our products. You deserve it.",
"ai_prompt": "An impact phrase that advertises the products"
}
],
"buttons": [
{
"default": "Shop jeans",
"ai_prompt": "The button text to go to the shop page"
}
]
}
},
{
"name": "Product Collection: Featured Products 5 Columns",
"slug": "woocommerce-blocks/product-collection-featured-products-5-columns",
"content": {
"titles": [
{
"default": "Shop new arrivals",
"ai_prompt": "An impact phrase that advertises the newest additions to the store"
}
]
}
}
]

View File

@@ -37,13 +37,12 @@ class Api {
public function __construct( PaymentMethodRegistry $payment_method_registry, AssetDataRegistry $asset_registry ) {
$this->payment_method_registry = $payment_method_registry;
$this->asset_registry = $asset_registry;
$this->init();
}
/**
* Initialize class features.
*/
protected function init() {
public function init() {
add_action( 'init', array( $this->payment_method_registry, 'initialize' ), 5 );
add_filter( 'woocommerce_blocks_register_script_dependencies', array( $this, 'add_payment_method_script_dependencies' ), 10, 2 );
add_action( 'woocommerce_blocks_checkout_enqueue_data', array( $this, 'add_payment_method_script_data' ) );
@@ -80,13 +79,13 @@ class Api {
* Add payment method data to Asset Registry.
*/
public function add_payment_method_script_data() {
// Enqueue the order of enabled gateways as `paymentGatewaySortOrder`.
if ( ! $this->asset_registry->exists( 'paymentGatewaySortOrder' ) ) {
// Enqueue the order of enabled gateways.
if ( ! $this->asset_registry->exists( 'paymentMethodSortOrder' ) ) {
// We use payment_gateways() here to get the sort order of all enabled gateways. Some may be
// programmatically disabled later on, but we still need to know where the enabled ones are in the list.
$payment_gateways = WC()->payment_gateways->payment_gateways();
$enabled_gateways = array_filter( $payment_gateways, array( $this, 'is_payment_gateway_enabled' ) );
$this->asset_registry->add( 'paymentGatewaySortOrder', array_keys( $enabled_gateways ) );
$this->asset_registry->add( 'paymentMethodSortOrder', array_keys( $enabled_gateways ) );
}
// Enqueue all registered gateway data (settings/config etc).

View File

@@ -59,9 +59,9 @@ final class PaymentMethodRegistry extends IntegrationRegistry {
$payment_methods = $this->get_all_active_registered();
foreach ( $payment_methods as $payment_method ) {
$script_data[ $payment_method->get_name() . '_data' ] = $payment_method->get_payment_method_data();
$script_data[ $payment_method->get_name() ] = $payment_method->get_payment_method_data();
}
return array_filter( $script_data );
return array( 'paymentMethodData' => array_filter( $script_data ) );
}
}

View File

@@ -76,12 +76,36 @@ class ShippingController {
add_filter( 'woocommerce_shipping_settings', array( $this, 'remove_shipping_settings' ) );
add_filter( 'wc_shipping_enabled', array( $this, 'force_shipping_enabled' ), 100, 1 );
add_filter( 'woocommerce_order_shipping_to_display', array( $this, 'show_local_pickup_details' ), 10, 2 );
add_filter( 'woocommerce_shipping_chosen_method', array( $this, 'prevent_shipping_method_selection_changes' ), 20, 3 );
// This is required to short circuit `show_shipping` from class-wc-cart.php - without it, that function
// returns based on the option's value in the DB and we can't override it any other way.
add_filter( 'option_woocommerce_shipping_cost_requires_address', array( $this, 'override_cost_requires_address_option' ) );
}
/**
* Prevent changes in the selected shipping method when new rates are added or removed.
*
* If the chosen method exists within package rates, it is returned to maintain the selection.
* Otherwise, the default rate is returned.
*
* @param string $default Default shipping method.
* @param array $package_rates Associative array of available package rates.
* @param string $chosen_method Previously chosen shipping method.
*
* @return string Chosen shipping method or default.
*/
public function prevent_shipping_method_selection_changes( $default, $package_rates, $chosen_method ) {
// If the chosen method exists in the package rates, return it.
if ( $chosen_method && isset( $package_rates[ $chosen_method ] ) ) {
return $chosen_method;
}
// Otherwise, return the default method.
return $default;
}
/**
* Overrides the option to force shipping calculations NOT to wait until an address is entered, but only if the
* Checkout page contains the Checkout Block.
@@ -147,52 +171,12 @@ class ShippingController {
}
/**
* If the Checkout block Remove shipping settings from WC Core's admin panels that are now block settings.
* When using the cart and checkout blocks this method is used to adjust core shipping settings via a filter hook.
*
* @param array $settings The default WC shipping settings.
* @return array|mixed The filtered settings with relevant items removed.
* @return array|mixed The filtered settings.
*/
public function remove_shipping_settings( $settings ) {
// Do not add the shipping calculator setting if the Cart block is not used on the WC cart page.
if ( CartCheckoutUtils::is_cart_block_default() ) {
// Ensure the 'Calculations' title is added to the `woocommerce_shipping_cost_requires_address` options
// group, since it is attached to the `woocommerce_enable_shipping_calc` option that gets removed if the
// Cart block is in use.
$calculations_title = '';
// Get Calculations title so we can add it to 'Hide shipping costs until an address is entered' option.
foreach ( $settings as $setting ) {
if ( 'woocommerce_enable_shipping_calc' === $setting['id'] ) {
$calculations_title = $setting['title'];
break;
}
}
// Add Calculations title to 'Hide shipping costs until an address is entered' option.
foreach ( $settings as $index => $setting ) {
if ( 'woocommerce_shipping_cost_requires_address' === $setting['id'] ) {
$settings[ $index ]['title'] = $calculations_title;
$settings[ $index ]['checkboxgroup'] = 'start';
break;
}
}
$settings = array_filter(
$settings,
function( $setting ) {
return ! in_array(
$setting['id'],
array(
'woocommerce_enable_shipping_calc',
),
true
);
}
);
}
if ( CartCheckoutUtils::is_checkout_block_default() && $this->local_pickup_enabled ) {
foreach ( $settings as $index => $setting ) {
if ( 'woocommerce_shipping_cost_requires_address' === $setting['id'] ) {

View File

@@ -114,15 +114,17 @@ abstract class AbstractCartRoute extends AbstractRoute {
}
}
if ( is_wp_error( $response ) ) {
$response = $this->error_to_response( $response );
}
// For update requests, this will recalculate cart totals and sync draft orders with the current cart.
if ( $this->is_update_request( $request ) ) {
$this->cart_updated( $request );
}
return $this->add_response_headers( $response );
// Format error responses.
if ( is_wp_error( $response ) ) {
$response = $this->error_to_response( $response );
}
return $this->add_response_headers( rest_ensure_response( $response ) );
}
/**
@@ -232,7 +234,9 @@ abstract class AbstractCartRoute extends AbstractRoute {
$draft_order = $this->get_draft_order();
if ( $draft_order ) {
$this->order_controller->update_order_from_cart( $draft_order );
// This does not trigger a recalculation of the cart--endpoints should have already done so before returning
// the cart response.
$this->order_controller->update_order_from_cart( $draft_order, false );
wc_do_deprecated_action(
'woocommerce_blocks_cart_update_order_from_request',

View File

@@ -163,9 +163,10 @@ abstract class AbstractRoute implements RouteInterface {
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response
*/
protected function get_route_response( \WP_REST_Request $request ) {
throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
return $this->get_route_error_response( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
}
/**
@@ -175,9 +176,10 @@ abstract class AbstractRoute implements RouteInterface {
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response
*/
protected function get_route_post_response( \WP_REST_Request $request ) {
throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
return $this->get_route_error_response( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
}
/**
@@ -187,9 +189,10 @@ abstract class AbstractRoute implements RouteInterface {
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response
*/
protected function get_route_update_response( \WP_REST_Request $request ) {
throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
return $this->get_route_error_response( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
}
/**
@@ -199,9 +202,10 @@ abstract class AbstractRoute implements RouteInterface {
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response
*/
protected function get_route_delete_response( \WP_REST_Request $request ) {
throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
return $this->get_route_error_response( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
}
/**

View File

@@ -69,8 +69,8 @@ class CartSelectShippingRate extends AbstractCartRoute {
}
$cart = $this->cart_controller->get_cart_instance();
$package_id = isset( $request['package_id'] ) ? wc_clean( wp_unslash( $request['package_id'] ) ) : null;
$rate_id = wc_clean( wp_unslash( $request['rate_id'] ) );
$package_id = isset( $request['package_id'] ) ? sanitize_text_field( wp_unslash( $request['package_id'] ) ) : null;
$rate_id = sanitize_text_field( wp_unslash( $request['rate_id'] ) );
try {
if ( ! is_null( $package_id ) ) {

View File

@@ -7,7 +7,7 @@ use Automattic\WooCommerce\StoreApi\Utilities\ValidationUtils;
/**
* CartUpdateCustomer class.
*
* Updates the customer billing and shipping address and returns an updated cart--things such as taxes may be recalculated.
* Updates the customer billing and shipping addresses, recalculates the cart totals, and returns an updated cart.
*/
class CartUpdateCustomer extends AbstractCartRoute {
use DraftOrderTrait;

View File

@@ -1,7 +1,6 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
use Automattic\WooCommerce\StoreApi\Payments\PaymentContext;
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
use Automattic\WooCommerce\StoreApi\Exceptions\InvalidStockLevelsInCartException;
use Automattic\WooCommerce\StoreApi\Exceptions\InvalidCartException;
@@ -9,12 +8,14 @@ use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
use Automattic\WooCommerce\Checkout\Helpers\ReserveStock;
use Automattic\WooCommerce\Checkout\Helpers\ReserveStockException;
use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait;
/**
* Checkout class.
*/
class Checkout extends AbstractCartRoute {
use DraftOrderTrait;
use CheckoutTrait;
/**
* The route identifier.
@@ -138,29 +139,6 @@ class Checkout extends AbstractCartRoute {
return $this->add_response_headers( $response );
}
/**
* Prepare a single item for response. Handles setting the status based on the payment result.
*
* @param mixed $item Item to format to schema.
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $item, \WP_REST_Request $request ) {
$response = parent::prepare_item_for_response( $item, $request );
$status_codes = [
'success' => 200,
'pending' => 202,
'failure' => 400,
'error' => 500,
];
if ( isset( $item->payment_result ) && $item->payment_result instanceof PaymentResult ) {
$response->set_status( $status_codes[ $item->payment_result->status ] ?? 200 );
}
return $response;
}
/**
* Convert the cart into a new draft order, or update an existing draft order, and return an updated cart response.
*
@@ -352,7 +330,7 @@ class Checkout extends AbstractCartRoute {
if ( ! $this->order ) {
$this->order = $this->order_controller->create_order_from_cart();
} else {
$this->order_controller->update_order_from_cart( $this->order );
$this->order_controller->update_order_from_cart( $this->order, true );
}
wc_do_deprecated_action(
@@ -466,133 +444,6 @@ class Checkout extends AbstractCartRoute {
$customer->save();
}
/**
* Update the current order using the posted values from the request.
*
* @param \WP_REST_Request $request Full details about the request.
*/
private function update_order_from_request( \WP_REST_Request $request ) {
$this->order->set_customer_note( $request['customer_note'] ?? '' );
$this->order->set_payment_method( $this->get_request_payment_method_id( $request ) );
$this->order->set_payment_method_title( $this->get_request_payment_method_title( $request ) );
wc_do_deprecated_action(
'__experimental_woocommerce_blocks_checkout_update_order_from_request',
array(
$this->order,
$request,
),
'6.3.0',
'woocommerce_store_api_checkout_update_order_from_request',
'This action was deprecated in WooCommerce Blocks version 6.3.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.'
);
wc_do_deprecated_action(
'woocommerce_blocks_checkout_update_order_from_request',
array(
$this->order,
$request,
),
'7.2.0',
'woocommerce_store_api_checkout_update_order_from_request',
'This action was deprecated in WooCommerce Blocks version 7.2.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.'
);
/**
* Fires when the Checkout Block/Store API updates an order's from the API request data.
*
* This hook gives extensions the chance to update orders based on the data in the request. This can be used in
* conjunction with the ExtendSchema class to post custom data and then process it.
*
* @since 7.2.0
*
* @param \WC_Order $order Order object.
* @param \WP_REST_Request $request Full details about the request.
*/
do_action( 'woocommerce_store_api_checkout_update_order_from_request', $this->order, $request );
$this->order->save();
}
/**
* For orders which do not require payment, just update status.
*
* @param \WP_REST_Request $request Request object.
* @param PaymentResult $payment_result Payment result object.
*/
private function process_without_payment( \WP_REST_Request $request, PaymentResult $payment_result ) {
// Transition the order to pending, and then completed. This ensures transactional emails fire for pending_to_complete events.
$this->order->update_status( 'pending' );
$this->order->payment_complete();
// Mark the payment as successful.
$payment_result->set_status( 'success' );
$payment_result->set_redirect_url( $this->order->get_checkout_order_received_url() );
}
/**
* Fires an action hook instructing active payment gateways to process the payment for an order and provide a result.
*
* @throws RouteException On error.
*
* @param \WP_REST_Request $request Request object.
* @param PaymentResult $payment_result Payment result object.
*/
private function process_payment( \WP_REST_Request $request, PaymentResult $payment_result ) {
try {
// Transition the order to pending before making payment.
$this->order->update_status( 'pending' );
// Prepare the payment context object to pass through payment hooks.
$context = new PaymentContext();
$context->set_payment_method( $this->get_request_payment_method_id( $request ) );
$context->set_payment_data( $this->get_request_payment_data( $request ) );
$context->set_order( $this->order );
/**
* Process payment with context.
*
* @hook woocommerce_rest_checkout_process_payment_with_context
*
* @throws \Exception If there is an error taking payment, an \Exception object can be thrown with an error message.
*
* @param PaymentContext $context Holds context for the payment, including order ID and payment method.
* @param PaymentResult $payment_result Result object for the transaction.
*/
do_action_ref_array( 'woocommerce_rest_checkout_process_payment_with_context', [ $context, &$payment_result ] );
if ( ! $payment_result instanceof PaymentResult ) {
throw new RouteException( 'woocommerce_rest_checkout_invalid_payment_result', __( 'Invalid payment result received from payment method.', 'woocommerce' ), 500 );
}
} catch ( \Exception $e ) {
throw new RouteException( 'woocommerce_rest_checkout_process_payment_error', $e->getMessage(), 402 );
}
}
/**
* Gets the chosen payment method ID from the request.
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return string
*/
private function get_request_payment_method_id( \WP_REST_Request $request ) {
$payment_method = $this->get_request_payment_method( $request );
return is_null( $payment_method ) ? '' : $payment_method->id;
}
/**
* Gets the chosen payment method title from the request.
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return string
*/
private function get_request_payment_method_title( \WP_REST_Request $request ) {
$payment_method = $this->get_request_payment_method( $request );
return is_null( $payment_method ) ? '' : $payment_method->get_title();
}
/**
* Gets the chosen payment method from the request.
*
@@ -633,26 +484,6 @@ class Checkout extends AbstractCartRoute {
return $available_gateways[ $request_payment_method ];
}
/**
* Gets and formats payment request data.
*
* @param \WP_REST_Request $request Request object.
* @return array
*/
private function get_request_payment_data( \WP_REST_Request $request ) {
static $payment_data = [];
if ( ! empty( $payment_data ) ) {
return $payment_data;
}
if ( ! empty( $request['payment_data'] ) ) {
foreach ( $request['payment_data'] as $data ) {
$payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] );
}
}
return $payment_data;
}
/**
* Order processing relating to customer account.
*

View File

@@ -0,0 +1,266 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
use Automattic\WooCommerce\StoreApi\Exceptions\InvalidStockLevelsInCartException;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\Utilities\OrderAuthorizationTrait;
use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait;
/**
* CheckoutOrder class.
*/
class CheckoutOrder extends AbstractCartRoute {
use OrderAuthorizationTrait;
use CheckoutTrait;
/**
* The route identifier.
*
* @var string
*/
const IDENTIFIER = 'checkout-order';
/**
* The routes schema.
*
* @var string
*/
const SCHEMA_TYPE = 'checkout-order';
/**
* Holds the current order being processed.
*
* @var \WC_Order
*/
private $order = null;
/**
* Get the path of this REST route.
*
* @return string
*/
public function get_path() {
return '/checkout/(?P<id>[\d]+)';
}
/**
* Get method arguments for this REST route.
*
* @return array An array of endpoints.
*/
public function get_args() {
return [
[
'methods' => \WP_REST_Server::CREATABLE,
'callback' => [ $this, 'get_response' ],
'permission_callback' => [ $this, 'is_authorized' ],
'args' => array_merge(
[
'payment_data' => [
'description' => __( 'Data to pass through to the payment method when processing payment.', 'woocommerce' ),
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'key' => [
'type' => 'string',
],
'value' => [
'type' => [ 'string', 'boolean' ],
],
],
],
],
],
$this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE )
),
],
'schema' => [ $this->schema, 'get_public_item_schema' ],
'allow_batch' => [ 'v1' => true ],
];
}
/**
* Process an order.
*
* 1. Process Request
* 2. Process Customer
* 3. Validate Order
* 4. Process Payment
*
* @throws RouteException On error.
* @throws InvalidStockLevelsInCartException On error.
*
* @param \WP_REST_Request $request Request object.
*
* @return \WP_REST_Response
*/
protected function get_route_post_response( \WP_REST_Request $request ) {
$order_id = absint( $request['id'] );
$this->order = wc_get_order( $order_id );
if ( $this->order->get_status() !== 'pending' && $this->order->get_status() !== 'failed' ) {
return new \WP_Error(
'invalid_order_update_status',
__( 'This order cannot be paid for.', 'woocommerce' )
);
}
/**
* Process request data.
*
* Note: Customer data is persisted from the request first so that OrderController::update_addresses_from_cart
* uses the up to date customer address.
*/
$this->update_billing_address( $request );
$this->update_order_from_request( $request );
/**
* Process customer data.
*
* Update order with customer details, and sign up a user account as necessary.
*/
$this->process_customer( $request );
/**
* Validate order.
*
* This logic ensures the order is valid before payment is attempted.
*/
$this->order_controller->validate_order_before_payment( $this->order );
/**
* Fires before an order is processed by the Checkout Block/Store API.
*
* This hook informs extensions that $order has completed processing and is ready for payment.
*
* This is similar to existing core hook woocommerce_checkout_order_processed. We're using a new action:
* - To keep the interface focused (only pass $order, not passing request data).
* - This also explicitly indicates these orders are from checkout block/StoreAPI.
*
* @since 7.2.0
*
* @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3238
* @example See docs/examples/checkout-order-processed.md
* @param \WC_Order $order Order object.
*/
do_action( 'woocommerce_store_api_checkout_order_processed', $this->order );
/**
* Process the payment and return the results.
*/
$payment_result = new PaymentResult();
if ( $this->order->needs_payment() ) {
$this->process_payment( $request, $payment_result );
} else {
$this->process_without_payment( $request, $payment_result );
}
return $this->prepare_item_for_response(
(object) [
'order' => wc_get_order( $this->order ),
'payment_result' => $payment_result,
],
$request
);
}
/**
* Updates the current customer session using data from the request (e.g. address data).
*
* Address session data is synced to the order itself later on by OrderController::update_order_from_cart()
*
* @param \WP_REST_Request $request Full details about the request.
*/
private function update_billing_address( \WP_REST_Request $request ) {
$customer = wc()->customer;
$billing = $request['billing_address'];
$shipping = $request['shipping_address'];
// Billing address is a required field.
foreach ( $billing as $key => $value ) {
if ( is_callable( [ $customer, "set_billing_$key" ] ) ) {
$customer->{"set_billing_$key"}( $value );
}
}
// If shipping address (optional field) was not provided, set it to the given billing address (required field).
$shipping_address_values = $shipping ?? $billing;
foreach ( $shipping_address_values as $key => $value ) {
if ( is_callable( [ $customer, "set_shipping_$key" ] ) ) {
$customer->{"set_shipping_$key"}( $value );
} elseif ( 'phone' === $key ) {
$customer->update_meta_data( 'shipping_phone', $value );
}
}
/**
* Fires when the Checkout Block/Store API updates a customer from the API request data.
*
* @since 8.2.0
*
* @param \WC_Customer $customer Customer object.
* @param \WP_REST_Request $request Full details about the request.
*/
do_action( 'woocommerce_store_api_checkout_update_customer_from_request', $customer, $request );
$customer->save();
$this->order->set_billing_address( $billing );
$this->order->set_shipping_address( $shipping );
$this->order->save();
$this->order->calculate_totals();
}
/**
* Gets the chosen payment method from the request.
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return \WC_Payment_Gateway|null
*/
private function get_request_payment_method( \WP_REST_Request $request ) {
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
$request_payment_method = wc_clean( wp_unslash( $request['payment_method'] ?? '' ) );
$requires_payment_method = $this->order->needs_payment();
if ( empty( $request_payment_method ) ) {
if ( $requires_payment_method ) {
throw new RouteException(
'woocommerce_rest_checkout_missing_payment_method',
__( 'No payment method provided.', 'woocommerce' ),
400
);
}
return null;
}
if ( ! isset( $available_gateways[ $request_payment_method ] ) ) {
throw new RouteException(
'woocommerce_rest_checkout_payment_method_disabled',
sprintf(
// Translators: %s Payment method ID.
__( 'The %s payment gateway is not available.', 'woocommerce' ),
esc_html( $request_payment_method )
),
400
);
}
return $available_gateways[ $request_payment_method ];
}
/**
* Updates the order with user details (e.g. address).
*
* @throws RouteException API error object with error details.
* @param \WP_REST_Request $request Request object.
*/
private function process_customer( \WP_REST_Request $request ) {
$this->order_controller->sync_customer_data_with_order( $this->order );
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\StoreApi\Schemas\v1\AbstractSchema;
use Automattic\WooCommerce\StoreApi\Utilities\OrderController;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\Utilities\OrderAuthorizationTrait;
/**
* Order class.
*/
class Order extends AbstractRoute {
use OrderAuthorizationTrait;
/**
* The route identifier.
*
* @var string
*/
const IDENTIFIER = 'order';
/**
* The schema item identifier.
*
* @var string
*/
const SCHEMA_TYPE = 'order';
/**
* Order controller class instance.
*
* @var OrderController
*/
protected $order_controller;
/**
* Constructor.
*
* @param SchemaController $schema_controller Schema Controller instance.
* @param AbstractSchema $schema Schema class for this route.
*/
public function __construct( SchemaController $schema_controller, AbstractSchema $schema ) {
parent::__construct( $schema_controller, $schema );
$this->order_controller = new OrderController();
}
/**
* Get the path of this REST route.
*
* @return string
*/
public function get_path() {
return '/order/(?P<id>[\d]+)';
}
/**
* Get method arguments for this REST route.
*
* @return array An array of endpoints.
*/
public function get_args() {
return [
[
'methods' => \WP_REST_Server::READABLE,
'callback' => [ $this, 'get_response' ],
'permission_callback' => [ $this, 'is_authorized' ],
'args' => [
'context' => $this->get_context_param( [ 'default' => 'view' ] ),
],
],
'schema' => [ $this->schema, 'get_public_item_schema' ],
];
}
/**
* Handle the request and return a valid response for this endpoint.
*
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response
*/
protected function get_route_response( \WP_REST_Request $request ) {
$order_id = absint( $request['id'] );
return rest_ensure_response( $this->schema->get_item_response( wc_get_order( $order_id ) ) );
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
use Automattic\WooCommerce\Blocks\AI\Connection;
use Automattic\WooCommerce\Blocks\Images\Pexels;
use Automattic\WooCommerce\Blocks\Patterns\PatternUpdater;
use Automattic\WooCommerce\Blocks\Patterns\ProductUpdater;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
/**
* Patterns class.
*/
class Patterns extends AbstractRoute {
/**
* The route identifier.
*
* @var string
*/
const IDENTIFIER = 'patterns';
/**
* The schema item identifier.
*
* @var string
*/
const SCHEMA_TYPE = 'patterns';
/**
* Get the path of this REST route.
*
* @return string
*/
public function get_path() {
return '/patterns';
}
/**
* Get method arguments for this REST route.
*
* @return array An array of endpoints.
*/
public function get_args() {
return [
[
'methods' => \WP_REST_Server::CREATABLE,
'callback' => [ $this, 'get_response' ],
'permission_callback' => [ $this, 'is_authorized' ],
'args' => [
'business_description' => [
'description' => __( 'The business description for a given store.', 'woocommerce' ),
'type' => 'string',
],
],
],
'schema' => [ $this->schema, 'get_public_item_schema' ],
'allow_batch' => [ 'v1' => true ],
];
}
/**
* Permission callback.
*
* @throws RouteException If the user is not allowed to make this request.
*
* @return true|\WP_Error
*/
public function is_authorized() {
try {
if ( ! current_user_can( 'manage_options' ) ) {
throw new RouteException( 'woocommerce_rest_invalid_user', __( 'You are not allowed to make this request. Please make sure you are logged in.', 'woocommerce' ), 403 );
}
} catch ( RouteException $error ) {
return new \WP_Error(
$error->getErrorCode(),
$error->getMessage(),
array( 'status' => $error->getCode() )
);
}
return true;
}
/**
* Ensure the content and images in patterns are powered by AI.
*
* @param \WP_REST_Request $request Request object.
*
* @return bool|string|\WP_Error|\WP_REST_Response
*/
protected function get_route_post_response( \WP_REST_Request $request ) {
$allow_ai_connection = get_option( 'woocommerce_blocks_allow_ai_connection' );
if ( ! $allow_ai_connection ) {
return rest_ensure_response(
$this->error_to_response(
new \WP_Error(
'ai_connection_not_allowed',
__( 'AI content generation is not allowed on this store. Update your store settings if you wish to enable this feature.', 'woocommerce' )
)
)
);
}
$business_description = sanitize_text_field( wp_unslash( $request['business_description'] ) );
if ( empty( $business_description ) ) {
$business_description = get_option( 'woo_ai_describe_store_description' );
}
$last_business_description = get_option( 'last_business_description_with_ai_content_generated' );
if ( $last_business_description === $business_description ) {
return rest_ensure_response(
$this->prepare_item_for_response(
[
'ai_content_generated' => true,
],
$request
)
);
}
$ai_connection = new Connection();
$site_id = $ai_connection->get_site_id();
if ( is_wp_error( $site_id ) ) {
return $site_id;
}
$token = $ai_connection->get_jwt_token( $site_id );
if ( is_wp_error( $token ) ) {
return $token;
}
$images = ( new Pexels() )->get_images( $ai_connection, $token, $business_description );
if ( is_wp_error( $images ) ) {
$response = $this->error_to_response( $images );
} else {
$populate_patterns = ( new PatternUpdater() )->generate_content( $ai_connection, $token, $images, $business_description );
if ( is_wp_error( $populate_patterns ) ) {
$response = $this->error_to_response( $populate_patterns );
}
$populate_products = ( new ProductUpdater() )->generate_content( $ai_connection, $token, $images, $business_description );
if ( is_wp_error( $populate_products ) ) {
$response = $this->error_to_response( $populate_products );
}
if ( true === $populate_patterns && true === $populate_products ) {
update_option( 'last_business_description_with_ai_content_generated', $business_description );
}
}
if ( ! isset( $response ) ) {
$response = $this->prepare_item_for_response(
[
'ai_content_generated' => true,
],
$request
);
}
return rest_ensure_response( $response );
}
}

View File

@@ -46,6 +46,17 @@ class ProductAttributeTerms extends AbstractTermsRoute {
];
}
/**
* Get the query params for collections of attributes.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['orderby']['enum'][] = 'menu_order';
return $params;
}
/**
* Get a collection of attribute terms.
*

View File

@@ -1,9 +1,8 @@
<?php
namespace Automattic\WooCommerce\StoreApi;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Exception;
use Routes\AbstractRoute;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractRoute;
/**
* RoutesController class.
@@ -47,6 +46,9 @@ class RoutesController {
Routes\V1\CartUpdateItem::IDENTIFIER => Routes\V1\CartUpdateItem::class,
Routes\V1\CartUpdateCustomer::IDENTIFIER => Routes\V1\CartUpdateCustomer::class,
Routes\V1\Checkout::IDENTIFIER => Routes\V1\Checkout::class,
Routes\V1\CheckoutOrder::IDENTIFIER => Routes\V1\CheckoutOrder::class,
Routes\V1\Order::IDENTIFIER => Routes\V1\Order::class,
Routes\V1\Patterns::IDENTIFIER => Routes\V1\Patterns::class,
Routes\V1\ProductAttributes::IDENTIFIER => Routes\V1\ProductAttributes::class,
Routes\V1\ProductAttributesById::IDENTIFIER => Routes\V1\ProductAttributesById::class,
Routes\V1\ProductAttributeTerms::IDENTIFIER => Routes\V1\ProductAttributeTerms::class,

View File

@@ -43,7 +43,13 @@ class SchemaController {
Schemas\V1\CartItemSchema::IDENTIFIER => Schemas\V1\CartItemSchema::class,
Schemas\V1\CartSchema::IDENTIFIER => Schemas\V1\CartSchema::class,
Schemas\V1\CartExtensionsSchema::IDENTIFIER => Schemas\V1\CartExtensionsSchema::class,
Schemas\V1\CheckoutOrderSchema::IDENTIFIER => Schemas\V1\CheckoutOrderSchema::class,
Schemas\V1\CheckoutSchema::IDENTIFIER => Schemas\V1\CheckoutSchema::class,
Schemas\V1\OrderItemSchema::IDENTIFIER => Schemas\V1\OrderItemSchema::class,
Schemas\V1\OrderCouponSchema::IDENTIFIER => Schemas\V1\OrderCouponSchema::class,
Schemas\V1\OrderFeeSchema::IDENTIFIER => Schemas\V1\OrderFeeSchema::class,
Schemas\V1\OrderSchema::IDENTIFIER => Schemas\V1\OrderSchema::class,
Schemas\V1\PatternsSchema::IDENTIFIER => Schemas\V1\PatternsSchema::class,
Schemas\V1\ProductSchema::IDENTIFIER => Schemas\V1\ProductSchema::class,
Schemas\V1\ProductAttributeSchema::IDENTIFIER => Schemas\V1\ProductAttributeSchema::class,
Schemas\V1\ProductCategorySchema::IDENTIFIER => Schemas\V1\ProductCategorySchema::class,

View File

@@ -63,6 +63,16 @@ abstract class AbstractSchema {
);
}
/**
* Returns the full item response.
*
* @param mixed $item Item to get response for.
* @return array|stdClass
*/
public function get_item_response( $item ) {
return [];
}
/**
* Return schema properties.
*

View File

@@ -87,7 +87,7 @@ class BillingAddressSchema extends AbstractAddressSchema {
* @param \WC_Order|\WC_Customer $address An object with billing address.
*
* @throws RouteException When the invalid object types are provided.
* @return stdClass
* @return array
*/
public function get_item_response( $address ) {
$validation_util = new ValidationUtils();
@@ -99,7 +99,7 @@ class BillingAddressSchema extends AbstractAddressSchema {
$billing_state = '';
}
return (object) $this->prepare_html_response(
return $this->prepare_html_response(
[
'first_name' => $address->get_billing_first_name(),
'last_name' => $address->get_billing_last_name(),

View File

@@ -1,14 +1,14 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
use Automattic\WooCommerce\StoreApi\Utilities\ProductItemTrait;
use Automattic\WooCommerce\StoreApi\Utilities\QuantityLimits;
/**
* CartItemSchema class.
*/
class CartItemSchema extends ProductSchema {
use DraftOrderTrait;
class CartItemSchema extends ItemSchema {
use ProductItemTrait;
/**
* The schema item name.
@@ -24,308 +24,6 @@ class CartItemSchema extends ProductSchema {
*/
const IDENTIFIER = 'cart-item';
/**
* Cart schema properties.
*
* @return array
*/
public function get_properties() {
return [
'key' => [
'description' => __( 'Unique identifier for the item within the cart.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'id' => [
'description' => __( 'The cart item product or variation ID.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity' => [
'description' => __( 'Quantity of this item in the cart.', 'woocommerce' ),
'type' => 'number',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity_limits' => [
'description' => __( 'How the quantity of this item should be controlled, for example, any limits in place.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'minimum' => [
'description' => __( 'The minimum quantity allowed in the cart for this line item.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'maximum' => [
'description' => __( 'The maximum quantity allowed in the cart for this line item.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'multiple_of' => [
'description' => __( 'The amount that quantities increment by. Quantity must be an multiple of this value.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'default' => 1,
],
'editable' => [
'description' => __( 'If the quantity in the cart is editable or fixed.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'default' => true,
],
],
],
'name' => [
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'short_description' => [
'description' => __( 'Product short description in HTML format.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'description' => [
'description' => __( 'Product full description in HTML format.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sku' => [
'description' => __( 'Stock keeping unit, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'low_stock_remaining' => [
'description' => __( 'Quantity left in stock if stock is low, or null if not applicable.', 'woocommerce' ),
'type' => [ 'integer', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'backorders_allowed' => [
'description' => __( 'True if backorders are allowed past stock availability.', 'woocommerce' ),
'type' => [ 'boolean' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'show_backorder_badge' => [
'description' => __( 'True if the product is on backorder.', 'woocommerce' ),
'type' => [ 'boolean' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sold_individually' => [
'description' => __( 'If true, only one item of this product is allowed for purchase in a single order.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'permalink' => [
'description' => __( 'Product URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'images' => [
'description' => __( 'List of images.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->image_attachment_schema->get_properties(),
],
],
'variation' => [
'description' => __( 'Chosen attributes (for variations).', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'attribute' => [
'description' => __( 'Variation attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'Variation attribute value.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'item_data' => [
'description' => __( 'Metadata related to the cart item', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'name' => [
'description' => __( 'Name of the metadata.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'Value of the metadata.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'display' => [
'description' => __( 'Optionally, how the metadata value should be displayed to the user.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'prices' => [
'description' => __( 'Price data for the product in the current line item, including or excluding taxes based on the "display prices during cart and checkout" setting. Provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'price' => [
'description' => __( 'Current product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'regular_price' => [
'description' => __( 'Regular product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sale_price' => [
'description' => __( 'Sale product price, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price_range' => [
'description' => __( 'Price range, if applicable.', 'woocommerce' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'min_amount' => [
'description' => __( 'Price amount.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'max_amount' => [
'description' => __( 'Price amount.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
'raw_prices' => [
'description' => __( 'Raw unrounded product prices used in calculations. Provided using a higher unit of precision than the currency.', 'woocommerce' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'precision' => [
'description' => __( 'Decimal precision of the returned prices.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price' => [
'description' => __( 'Current product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'regular_price' => [
'description' => __( 'Regular product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sale_price' => [
'description' => __( 'Sale product price, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
]
),
],
'totals' => [
'description' => __( 'Item total amounts provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'line_subtotal' => [
'description' => __( 'Line subtotal (the price of the product before coupon discounts have been applied).', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_subtotal_tax' => [
'description' => __( 'Line subtotal tax.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total' => [
'description' => __( 'Line total (the price of the product after coupon discounts have been applied).', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total_tax' => [
'description' => __( 'Line total tax.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
]
),
],
'catalog_visibility' => [
'description' => __( 'Whether the product is visible in the catalog', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ),
];
}
/**
* Convert a WooCommerce cart item to an object suitable for the response.
*
@@ -352,6 +50,7 @@ class CartItemSchema extends ProductSchema {
return [
'key' => $cart_item['key'],
'id' => $product->get_id(),
'type' => $product->get_type(),
'quantity' => wc_stock_amount( $cart_item['quantity'] ),
'quantity_limits' => (object) ( new QuantityLimits() )->get_cart_item_quantity_limits( $cart_item ),
'name' => $this->prepare_html_response( $product->get_title() ),
@@ -380,82 +79,6 @@ class CartItemSchema extends ProductSchema {
];
}
/**
* Get an array of pricing data.
*
* @param \WC_Product $product Product instance.
* @param string $tax_display_mode If returned prices are incl or excl of tax.
* @return array
*/
protected function prepare_product_price_response( \WC_Product $product, $tax_display_mode = '' ) {
$tax_display_mode = $this->get_tax_display_mode( $tax_display_mode );
$price_function = $this->get_price_function_from_tax_display_mode( $tax_display_mode );
$prices = parent::prepare_product_price_response( $product, $tax_display_mode );
// Add raw prices (prices with greater precision).
$prices['raw_prices'] = [
'precision' => wc_get_rounding_precision(),
'price' => $this->prepare_money_response( $price_function( $product ), wc_get_rounding_precision() ),
'regular_price' => $this->prepare_money_response( $price_function( $product, [ 'price' => $product->get_regular_price() ] ), wc_get_rounding_precision() ),
'sale_price' => $this->prepare_money_response( $price_function( $product, [ 'price' => $product->get_sale_price() ] ), wc_get_rounding_precision() ),
];
return $prices;
}
/**
* Format variation data, for example convert slugs such as attribute_pa_size to Size.
*
* @param array $variation_data Array of data from the cart.
* @param \WC_Product $product Product data.
* @return array
*/
protected function format_variation_data( $variation_data, $product ) {
$return = [];
if ( ! is_iterable( $variation_data ) ) {
return $return;
}
foreach ( $variation_data as $key => $value ) {
$taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_pa_', '', urldecode( $key ) ) );
if ( taxonomy_exists( $taxonomy ) ) {
// If this is a term slug, get the term's nice name.
$term = get_term_by( 'slug', $value, $taxonomy );
if ( ! is_wp_error( $term ) && $term && $term->name ) {
$value = $term->name;
}
$label = wc_attribute_label( $taxonomy );
} else {
/**
* Filters the variation option name.
*
* Filters the variation option name for custom option slugs.
*
* @since 2.5.0
*
* @internal Matches filter name in WooCommerce core.
*
* @param string $value The name to display.
* @param null $unused Unused because this is not a variation taxonomy.
* @param string $taxonomy Taxonomy or product attribute name.
* @param \WC_Product $product Product data.
* @return string
*/
$value = apply_filters( 'woocommerce_variation_option_name', $value, null, $taxonomy, $product );
$label = wc_attribute_label( str_replace( 'attribute_', '', $key ), $product );
}
$return[] = [
'attribute' => $this->prepare_html_response( $label ),
'value' => $this->prepare_html_response( $value ),
];
}
return $return;
}
/**
* Format cart item data removing any HTML tag.
*

View File

@@ -349,42 +349,52 @@ class CartSchema extends AbstractSchema {
$cross_sells = array_filter( array_map( 'wc_get_product', $cart->get_cross_sells() ), 'wc_products_array_filter_visible' );
return [
'coupons' => $this->get_item_responses_from_schema( $this->coupon_schema, $cart->get_applied_coupons() ),
'shipping_rates' => $this->get_item_responses_from_schema( $this->shipping_rate_schema, $shipping_packages ),
'shipping_address' => $this->shipping_address_schema->get_item_response( wc()->customer ),
'billing_address' => $this->billing_address_schema->get_item_response( wc()->customer ),
'items' => $this->get_item_responses_from_schema( $this->item_schema, $cart->get_cart() ),
'coupons' => $this->get_item_responses_from_schema( $this->coupon_schema, $cart->get_applied_coupons() ),
'fees' => $this->get_item_responses_from_schema( $this->fee_schema, $cart->get_fees() ),
'totals' => (object) $this->prepare_currency_response( $this->get_totals( $cart ) ),
'shipping_address' => (object) $this->shipping_address_schema->get_item_response( wc()->customer ),
'billing_address' => (object) $this->billing_address_schema->get_item_response( wc()->customer ),
'needs_payment' => $cart->needs_payment(),
'needs_shipping' => $cart->needs_shipping(),
'payment_requirements' => $this->extend->get_payment_requirements(),
'has_calculated_shipping' => $has_calculated_shipping,
'shipping_rates' => $this->get_item_responses_from_schema( $this->shipping_rate_schema, $shipping_packages ),
'items_count' => $cart->get_cart_contents_count(),
'items_weight' => wc_get_weight( $cart->get_cart_contents_weight(), 'g' ),
'cross_sells' => $this->get_item_responses_from_schema( $this->cross_sells_item_schema, $cross_sells ),
'needs_payment' => $cart->needs_payment(),
'needs_shipping' => $cart->needs_shipping(),
'has_calculated_shipping' => $has_calculated_shipping,
'fees' => $this->get_item_responses_from_schema( $this->fee_schema, $cart->get_fees() ),
'totals' => (object) $this->prepare_currency_response(
[
'total_items' => $this->prepare_money_response( $cart->get_subtotal(), wc_get_price_decimals() ),
'total_items_tax' => $this->prepare_money_response( $cart->get_subtotal_tax(), wc_get_price_decimals() ),
'total_fees' => $this->prepare_money_response( $cart->get_fee_total(), wc_get_price_decimals() ),
'total_fees_tax' => $this->prepare_money_response( $cart->get_fee_tax(), wc_get_price_decimals() ),
'total_discount' => $this->prepare_money_response( $cart->get_discount_total(), wc_get_price_decimals() ),
'total_discount_tax' => $this->prepare_money_response( $cart->get_discount_tax(), wc_get_price_decimals() ),
'total_shipping' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_total(), wc_get_price_decimals() ) : null,
'total_shipping_tax' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_tax(), wc_get_price_decimals() ) : null,
// Explicitly request context='edit'; default ('view') will render total as markup.
'total_price' => $this->prepare_money_response( $cart->get_total( 'edit' ), wc_get_price_decimals() ),
'total_tax' => $this->prepare_money_response( $cart->get_total_tax(), wc_get_price_decimals() ),
'tax_lines' => $this->get_tax_lines( $cart ),
]
),
'errors' => $cart_errors,
'payment_methods' => array_values( wp_list_pluck( WC()->payment_gateways->get_available_payment_gateways(), 'id' ) ),
'payment_requirements' => $this->extend->get_payment_requirements(),
self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER ),
];
}
/**
* Get total data.
*
* @param \WC_Cart $cart Cart class instance.
* @return array
*/
protected function get_totals( $cart ) {
$has_calculated_shipping = $cart->show_shipping();
$decimals = wc_get_price_decimals();
return [
'total_items' => $this->prepare_money_response( $cart->get_subtotal(), $decimals ),
'total_items_tax' => $this->prepare_money_response( $cart->get_subtotal_tax(), $decimals ),
'total_fees' => $this->prepare_money_response( $cart->get_fee_total(), $decimals ),
'total_fees_tax' => $this->prepare_money_response( $cart->get_fee_tax(), $decimals ),
'total_discount' => $this->prepare_money_response( $cart->get_discount_total(), $decimals ),
'total_discount_tax' => $this->prepare_money_response( $cart->get_discount_tax(), $decimals ),
'total_shipping' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_total(), $decimals ) : null,
'total_shipping_tax' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_tax(), $decimals ) : null,
// Explicitly request context='edit'; default ('view') will render total as markup.
'total_price' => $this->prepare_money_response( $cart->get_total( 'edit' ), $decimals ),
'total_tax' => $this->prepare_money_response( $cart->get_total_tax(), $decimals ),
'tax_lines' => $this->get_tax_lines( $cart ),
];
}
/**
* Get tax lines from the cart and format to match schema.
*
@@ -399,11 +409,12 @@ class CartSchema extends AbstractSchema {
}
$cart_tax_totals = $cart->get_tax_totals();
$decimals = wc_get_price_decimals();
foreach ( $cart_tax_totals as $cart_tax_total ) {
$tax_lines[] = array(
'name' => $cart_tax_total->label,
'price' => $this->prepare_money_response( $cart_tax_total->amount, wc_get_price_decimals() ),
'price' => $this->prepare_money_response( $cart_tax_total->amount, $decimals ),
'rate' => WC_Tax::get_rate_percent( $cart_tax_total->tax_rate_id ),
);
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
/**
* CheckoutOrderSchema class.
*/
class CheckoutOrderSchema extends CheckoutSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'checkout-order';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'checkout-order';
/**
* Checkout schema properties.
*
* @return array
*/
public function get_properties() {
$parent_properties = parent::get_properties();
unset( $parent_properties['create_account'] );
return $parent_properties;
}
}

View File

@@ -197,8 +197,8 @@ class CheckoutSchema extends AbstractSchema {
'order_number' => $order->get_order_number(),
'customer_note' => $order->get_customer_note(),
'customer_id' => $order->get_customer_id(),
'billing_address' => $this->billing_address_schema->get_item_response( $order ),
'shipping_address' => $this->shipping_address_schema->get_item_response( $order ),
'billing_address' => (object) $this->billing_address_schema->get_item_response( $order ),
'shipping_address' => (object) $this->shipping_address_schema->get_item_response( $order ),
'payment_method' => $order->get_payment_method(),
'payment_result' => [
'payment_status' => $payment_result->status,

View File

@@ -47,7 +47,7 @@ class ErrorSchema extends AbstractSchema {
* @param \WP_Error $error Error object.
* @return array
*/
public function get_item_response( \WP_Error $error ) {
public function get_item_response( $error ) {
return [
'code' => $this->prepare_html_response( $error->get_error_code() ),
'message' => $this->prepare_html_response( $error->get_error_message() ),

View File

@@ -70,7 +70,7 @@ class ImageAttachmentSchema extends AbstractSchema {
* Convert a WooCommerce product into an object suitable for the response.
*
* @param int $attachment_id Image attachment ID.
* @return array|null
* @return object|null
*/
public function get_item_response( $attachment_id ) {
if ( ! $attachment_id ) {
@@ -80,12 +80,12 @@ class ImageAttachmentSchema extends AbstractSchema {
$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
if ( ! is_array( $attachment ) ) {
return [];
return null;
}
$thumbnail = wp_get_attachment_image_src( $attachment_id, 'woocommerce_thumbnail' );
return [
return (object) [
'id' => (int) $attachment_id,
'src' => current( $attachment ),
'thumbnail' => current( $thumbnail ),

View File

@@ -0,0 +1,316 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
/**
* ItemSchema class.
*/
abstract class ItemSchema extends ProductSchema {
/**
* Item schema properties.
*
* @return array
*/
public function get_properties() {
return [
'key' => [
'description' => __( 'Unique identifier for the item.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'type' => [
'description' => __( 'The item type.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'id' => [
'description' => __( 'The item product or variation ID.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity' => [
'description' => __( 'Quantity of this item.', 'woocommerce' ),
'type' => 'number',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity_limits' => [
'description' => __( 'How the quantity of this item should be controlled, for example, any limits in place.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'minimum' => [
'description' => __( 'The minimum quantity allowed for this line item.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'maximum' => [
'description' => __( 'The maximum quantity allowed for this line item.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'multiple_of' => [
'description' => __( 'The amount that quantities increment by. Quantity must be an multiple of this value.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'default' => 1,
],
'editable' => [
'description' => __( 'If the quantity is editable or fixed.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'default' => true,
],
],
],
'name' => [
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'short_description' => [
'description' => __( 'Product short description in HTML format.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'description' => [
'description' => __( 'Product full description in HTML format.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sku' => [
'description' => __( 'Stock keeping unit, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'low_stock_remaining' => [
'description' => __( 'Quantity left in stock if stock is low, or null if not applicable.', 'woocommerce' ),
'type' => [ 'integer', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'backorders_allowed' => [
'description' => __( 'True if backorders are allowed past stock availability.', 'woocommerce' ),
'type' => [ 'boolean' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'show_backorder_badge' => [
'description' => __( 'True if the product is on backorder.', 'woocommerce' ),
'type' => [ 'boolean' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sold_individually' => [
'description' => __( 'If true, only one item of this product is allowed for purchase in a single order.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'permalink' => [
'description' => __( 'Product URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'images' => [
'description' => __( 'List of images.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->image_attachment_schema->get_properties(),
],
],
'variation' => [
'description' => __( 'Chosen attributes (for variations).', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'attribute' => [
'description' => __( 'Variation attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'Variation attribute value.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'item_data' => [
'description' => __( 'Metadata related to the item', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'name' => [
'description' => __( 'Name of the metadata.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'Value of the metadata.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'display' => [
'description' => __( 'Optionally, how the metadata value should be displayed to the user.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'prices' => [
'description' => __( 'Price data for the product in the current line item, including or excluding taxes based on the "display prices during cart and checkout" setting. Provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'price' => [
'description' => __( 'Current product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'regular_price' => [
'description' => __( 'Regular product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sale_price' => [
'description' => __( 'Sale product price, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price_range' => [
'description' => __( 'Price range, if applicable.', 'woocommerce' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'min_amount' => [
'description' => __( 'Price amount.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'max_amount' => [
'description' => __( 'Price amount.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
'raw_prices' => [
'description' => __( 'Raw unrounded product prices used in calculations. Provided using a higher unit of precision than the currency.', 'woocommerce' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'precision' => [
'description' => __( 'Decimal precision of the returned prices.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price' => [
'description' => __( 'Current product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'regular_price' => [
'description' => __( 'Regular product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sale_price' => [
'description' => __( 'Sale product price, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
]
),
],
'totals' => [
'description' => __( 'Item total amounts provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'line_subtotal' => [
'description' => __( 'Line subtotal (the price of the product before coupon discounts have been applied).', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_subtotal_tax' => [
'description' => __( 'Line subtotal tax.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total' => [
'description' => __( 'Line total (the price of the product after coupon discounts have been applied).', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total_tax' => [
'description' => __( 'Line total tax.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
]
),
],
'catalog_visibility' => [
'description' => __( 'Whether the product is visible in the catalog', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ),
];
}
}

View File

@@ -26,13 +26,19 @@ class OrderCouponSchema extends AbstractSchema {
*/
public function get_properties() {
return [
'code' => [
'code' => [
'description' => __( 'The coupons unique code.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'totals' => [
'discount_type' => [
'description' => __( 'The discount type for the coupon (e.g. percentage or fixed amount)', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'totals' => [
'description' => __( 'Total amounts provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
@@ -61,13 +67,15 @@ class OrderCouponSchema extends AbstractSchema {
/**
* Convert an order coupon to an object suitable for the response.
*
* @param \WC_Order_Item_Coupon $coupon Order coupon array.
* @param \WC_Order_Item_Coupon $coupon Order coupon object.
* @return array
*/
public function get_item_response( \WC_Order_Item_Coupon $coupon ) {
public function get_item_response( $coupon ) {
$coupon_object = new \WC_Coupon( $coupon->get_code() );
return [
'code' => $coupon->get_code(),
'totals' => (object) $this->prepare_currency_response(
'code' => $coupon->get_code(),
'discount_type' => $coupon_object ? $coupon_object->get_discount_type() : '',
'totals' => (object) $this->prepare_currency_response(
[
'total_discount' => $this->prepare_money_response( $coupon->get_discount(), wc_get_price_decimals() ),
'total_discount_tax' => $this->prepare_money_response( $coupon->get_discount_tax(), wc_get_price_decimals(), PHP_ROUND_HALF_DOWN ),

View File

@@ -0,0 +1,88 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
/**
* OrderFeeSchema class.
*/
class OrderFeeSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'order_fee';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'order-fee';
/**
* Cart schema properties.
*
* @return array
*/
public function get_properties() {
return [
'id' => [
'description' => __( 'Unique identifier for the fee within the cart', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'name' => [
'description' => __( 'Fee name', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'totals' => [
'description' => __( 'Fee total amounts provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'total' => [
'description' => __( 'Total amount for this fee.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_tax' => [
'description' => __( 'Total tax amount for this fee.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
]
),
],
];
}
/**
* Convert a WooCommerce cart fee to an object suitable for the response.
*
* @param \WC_Order_Item_Fee $fee Order fee object.
* @return array
*/
public function get_item_response( $fee ) {
if ( ! $fee ) {
return [];
}
return [
'key' => $fee->get_id(),
'name' => $this->prepare_html_response( $fee->get_name() ),
'totals' => (object) $this->prepare_currency_response(
[
'total' => $this->prepare_money_response( $fee->get_total(), wc_get_price_decimals() ),
'total_tax' => $this->prepare_money_response( $fee->get_total_tax(), wc_get_price_decimals(), PHP_ROUND_HALF_DOWN ),
]
),
];
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
use Automattic\WooCommerce\StoreApi\Utilities\ProductItemTrait;
/**
* OrderItemSchema class.
*/
class OrderItemSchema extends ItemSchema {
use ProductItemTrait;
/**
* The schema item name.
*
* @var string
*/
protected $title = 'order_item';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'order-item';
/**
* Get order items data.
*
* @param \WC_Order_Item_Product $order_item Order item instance.
* @return array
*/
public function get_item_response( $order_item ) {
$order = $order_item->get_order();
$product = $order_item->get_product();
return [
'key' => $order->get_order_key(),
'id' => $order_item->get_id(),
'quantity' => $order_item->get_quantity(),
'quantity_limits' => array(
'minimum' => $order_item->get_quantity(),
'maximum' => $order_item->get_quantity(),
'multiple_of' => 1,
'editable' => false,
),
'name' => $order_item->get_name(),
'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_short_description() ) ) ),
'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_description() ) ) ),
'sku' => $this->prepare_html_response( $product->get_sku() ),
'low_stock_remaining' => null,
'backorders_allowed' => false,
'show_backorder_badge' => false,
'sold_individually' => $product->is_sold_individually(),
'permalink' => $product->get_permalink(),
'images' => $this->get_images( $product ),
'variation' => $this->format_variation_data( $product->get_attributes(), $product ),
'item_data' => $order_item->get_all_formatted_meta_data(),
'prices' => (object) $this->prepare_product_price_response( $product, get_option( 'woocommerce_tax_display_cart' ) ),
'totals' => (object) $this->prepare_currency_response( $this->get_totals( $order_item ) ),
'catalog_visibility' => $product->get_catalog_visibility(),
];
}
/**
* Get totals data.
*
* @param \WC_Order_Item_Product $order_item Order item instance.
* @return array
*/
public function get_totals( $order_item ) {
return [
'line_subtotal' => $this->prepare_money_response( $order_item->get_subtotal(), wc_get_price_decimals() ),
'line_subtotal_tax' => $this->prepare_money_response( $order_item->get_subtotal_tax(), wc_get_price_decimals() ),
'line_total' => $this->prepare_money_response( $order_item->get_total(), wc_get_price_decimals() ),
'line_total_tax' => $this->prepare_money_response( $order_item->get_total_tax(), wc_get_price_decimals() ),
];
}
}

View File

@@ -0,0 +1,391 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
use Automattic\WooCommerce\StoreApi\Utilities\OrderController;
/**
* OrderSchema class.
*/
class OrderSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'order';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'order';
/**
* Item schema instance.
*
* @var OrderItemSchema
*/
public $item_schema;
/**
* Order controller class instance.
*
* @var OrderController
*/
protected $order_controller;
/**
* Coupon schema instance.
*
* @var OrderCouponSchema
*/
public $coupon_schema;
/**
* Product item schema instance representing cross-sell items.
*
* @var ProductSchema
*/
public $cross_sells_item_schema;
/**
* Fee schema instance.
*
* @var OrderFeeSchema
*/
public $fee_schema;
/**
* Shipping rates schema instance.
*
* @var CartShippingRateSchema
*/
public $shipping_rate_schema;
/**
* Shipping address schema instance.
*
* @var ShippingAddressSchema
*/
public $shipping_address_schema;
/**
* Billing address schema instance.
*
* @var BillingAddressSchema
*/
public $billing_address_schema;
/**
* Error schema instance.
*
* @var ErrorSchema
*/
public $error_schema;
/**
* Constructor.
*
* @param ExtendSchema $extend Rest Extending instance.
* @param SchemaController $controller Schema Controller instance.
*/
public function __construct( ExtendSchema $extend, SchemaController $controller ) {
parent::__construct( $extend, $controller );
$this->item_schema = $this->controller->get( OrderItemSchema::IDENTIFIER );
$this->coupon_schema = $this->controller->get( OrderCouponSchema::IDENTIFIER );
$this->fee_schema = $this->controller->get( OrderFeeSchema::IDENTIFIER );
$this->shipping_rate_schema = $this->controller->get( CartShippingRateSchema::IDENTIFIER );
$this->shipping_address_schema = $this->controller->get( ShippingAddressSchema::IDENTIFIER );
$this->billing_address_schema = $this->controller->get( BillingAddressSchema::IDENTIFIER );
$this->error_schema = $this->controller->get( ErrorSchema::IDENTIFIER );
$this->order_controller = new OrderController();
}
/**
* Order schema properties.
*
* @return array
*/
public function get_properties() {
return [
'id' => [
'description' => __( 'The order ID.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'items' => [
'description' => __( 'Line items data.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( $this->item_schema->get_properties() ),
],
],
'totals' => [
'description' => __( 'Order totals.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'subtotal' => [
'description' => __( 'Subtotal of the order.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_discount' => [
'description' => __( 'Total discount from applied coupons.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_shipping' => [
'description' => __( 'Total price of shipping.', 'woocommerce' ),
'type' => [ 'string', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_fees' => [
'description' => __( 'Total price of any applied fees.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_tax' => [
'description' => __( 'Total tax applied to the order.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_refund' => [
'description' => __( 'Total refund applied to the order.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_price' => [
'description' => __( 'Total price the customer will pay.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_items' => [
'description' => __( 'Total price of items in the order.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_items_tax' => [
'description' => __( 'Total tax on items in the order.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_fees_tax' => [
'description' => __( 'Total tax on fees.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_discount_tax' => [
'description' => __( 'Total tax removed due to discount from applied coupons.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_shipping_tax' => [
'description' => __( 'Total tax on shipping. If shipping has not been calculated, a null response will be sent.', 'woocommerce' ),
'type' => [ 'string', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'tax_lines' => [
'description' => __( 'Lines of taxes applied to items and shipping.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'name' => [
'description' => __( 'The name of the tax.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price' => [
'description' => __( 'The amount of tax charged.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'rate' => [
'description' => __( 'The rate at which tax is applied.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
]
),
],
'coupons' => [
'description' => __( 'List of applied cart coupons.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( $this->coupon_schema->get_properties() ),
],
],
'shipping_address' => [
'description' => __( 'Current set shipping address for the customer.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => $this->force_schema_readonly( $this->shipping_address_schema->get_properties() ),
],
'billing_address' => [
'description' => __( 'Current set billing address for the customer.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => $this->force_schema_readonly( $this->billing_address_schema->get_properties() ),
],
'needs_payment' => [
'description' => __( 'True if the cart needs payment. False for carts with only free products and no shipping costs.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'needs_shipping' => [
'description' => __( 'True if the cart needs shipping. False for carts with only digital goods or stores with no shipping methods set-up.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'errors' => [
'description' => __( 'List of cart item errors, for example, items in the cart which are out of stock.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( $this->error_schema->get_properties() ),
],
],
'payment_requirements' => [
'description' => __( 'List of required payment gateway features to process the order.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'status' => [
'description' => __( 'Status of the order.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
];
}
/**
* Get an order for response.
*
* @param \WC_Order $order Order instance.
* @return array
*/
public function get_item_response( $order ) {
$order_id = $order->get_id();
$errors = [];
$failed_order_stock_error = $this->order_controller->get_failed_order_stock_error( $order_id );
if ( $failed_order_stock_error ) {
$errors[] = $failed_order_stock_error;
}
return [
'id' => $order_id,
'status' => $order->get_status(),
'items' => $this->get_item_responses_from_schema( $this->item_schema, $order->get_items() ),
'coupons' => $this->get_item_responses_from_schema( $this->coupon_schema, $order->get_items( 'coupon' ) ),
'fees' => $this->get_item_responses_from_schema( $this->fee_schema, $order->get_items( 'fee' ) ),
'totals' => (object) $this->prepare_currency_response( $this->get_totals( $order ) ),
'shipping_address' => (object) $this->shipping_address_schema->get_item_response( $order ),
'billing_address' => (object) $this->billing_address_schema->get_item_response( $order ),
'needs_payment' => $order->needs_payment(),
'needs_shipping' => $order->needs_shipping_address(),
'payment_requirements' => $this->extend->get_payment_requirements(),
'errors' => $errors,
];
}
/**
* Get total data.
*
* @param \WC_Order $order Order instance.
* @return array
*/
protected function get_totals( $order ) {
return [
'subtotal' => $this->prepare_money_response( $order->get_subtotal() ),
'total_discount' => $this->prepare_money_response( $order->get_total_discount() ),
'total_shipping' => $this->prepare_money_response( $order->get_total_shipping() ),
'total_fees' => $this->prepare_money_response( $order->get_total_fees() ),
'total_tax' => $this->prepare_money_response( $order->get_total_tax() ),
'total_refund' => $this->prepare_money_response( $order->get_total_refunded() ),
'total_price' => $this->prepare_money_response( $order->get_total() ),
'total_items' => $this->prepare_money_response(
array_sum(
array_map(
function( $item ) {
return $item->get_total();
},
array_values( $order->get_items( 'line_item' ) )
)
)
),
'total_items_tax' => $this->prepare_money_response(
array_sum(
array_map(
function( $item ) {
return $item->get_tax_total();
},
array_values( $order->get_items( 'tax' ) )
)
)
),
'total_fees_tax' => $this->prepare_money_response(
array_sum(
array_map(
function( $item ) {
return $item->get_total_tax();
},
array_values( $order->get_items( 'fee' ) )
)
)
),
'total_discount_tax' => $this->prepare_money_response( $order->get_discount_tax() ),
'total_shipping_tax' => $this->prepare_money_response( $order->get_shipping_tax() ),
'tax_lines' => array_map(
function( $item ) {
return [
'name' => $item->get_name(),
'price' => $item->get_tax_total(),
'rate' => strval( $item->get_rate_percent() ),
];
},
array_values( $order->get_items( 'tax' ) )
),
];
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
/**
* OrderSchema class.
*/
class PatternsSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'patterns';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'patterns';
/**
* Patterns schema properties.
*
* @return array
*/
public function get_properties() {
return [];
}
/**
* Get the Patterns response.
*
* @param array $item Item to get response for.
*
* @return array
*/
public function get_item_response( $item ) {
return [
'ai_content_generated' => true,
];
}
}

Some files were not shown because too many files have changed in this diff Show More