plugin updates

This commit is contained in:
Tony Volpe
2024-09-05 11:04:01 -04:00
parent ed6b060261
commit 50cd64dd3d
925 changed files with 16918 additions and 13003 deletions

View File

@@ -57,83 +57,98 @@ class Init {
* Init REST API.
*/
public function rest_api_init() {
$controllers = array(
'Automattic\WooCommerce\Admin\API\Features',
'Automattic\WooCommerce\Admin\API\Notes',
'Automattic\WooCommerce\Admin\API\NoteActions',
'Automattic\WooCommerce\Admin\API\Coupons',
'Automattic\WooCommerce\Admin\API\Data',
'Automattic\WooCommerce\Admin\API\DataCountries',
'Automattic\WooCommerce\Admin\API\DataDownloadIPs',
'Automattic\WooCommerce\Admin\API\Experiments',
'Automattic\WooCommerce\Admin\API\Marketing',
'Automattic\WooCommerce\Admin\API\MarketingOverview',
'Automattic\WooCommerce\Admin\API\MarketingRecommendations',
'Automattic\WooCommerce\Admin\API\MarketingChannels',
'Automattic\WooCommerce\Admin\API\MarketingCampaigns',
'Automattic\WooCommerce\Admin\API\MarketingCampaignTypes',
'Automattic\WooCommerce\Admin\API\Notice',
'Automattic\WooCommerce\Admin\API\Options',
'Automattic\WooCommerce\Admin\API\Orders',
'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions',
'Automattic\WooCommerce\Admin\API\Products',
'Automattic\WooCommerce\Admin\API\ProductAttributes',
'Automattic\WooCommerce\Admin\API\ProductAttributeTerms',
'Automattic\WooCommerce\Admin\API\ProductCategories',
'Automattic\WooCommerce\Admin\API\ProductVariations',
'Automattic\WooCommerce\Admin\API\ProductReviews',
'Automattic\WooCommerce\Admin\API\ProductVariations',
'Automattic\WooCommerce\Admin\API\ProductsLowInStock',
'Automattic\WooCommerce\Admin\API\SettingOptions',
'Automattic\WooCommerce\Admin\API\Themes',
'Automattic\WooCommerce\Admin\API\Plugins',
'Automattic\WooCommerce\Admin\API\OnboardingFreeExtensions',
'Automattic\WooCommerce\Admin\API\OnboardingProductTypes',
'Automattic\WooCommerce\Admin\API\OnboardingProfile',
'Automattic\WooCommerce\Admin\API\OnboardingTasks',
'Automattic\WooCommerce\Admin\API\OnboardingThemes',
'Automattic\WooCommerce\Admin\API\OnboardingPlugins',
'Automattic\WooCommerce\Admin\API\OnboardingProducts',
'Automattic\WooCommerce\Admin\API\NavigationFavorites',
'Automattic\WooCommerce\Admin\API\Taxes',
'Automattic\WooCommerce\Admin\API\MobileAppMagicLink',
'Automattic\WooCommerce\Admin\API\ShippingPartnerSuggestions',
);
$controllers = array();
$analytics_controllers = array();
if ( wc_rest_should_load_namespace( 'wc-admin' ) ) {
// Controllers in the wc-admin namespace.
$controllers = array(
'Automattic\WooCommerce\Admin\API\Notice',
'Automattic\WooCommerce\Admin\API\Features',
'Automattic\WooCommerce\Admin\API\Experiments',
'Automattic\WooCommerce\Admin\API\Marketing',
'Automattic\WooCommerce\Admin\API\MarketingOverview',
'Automattic\WooCommerce\Admin\API\MarketingRecommendations',
'Automattic\WooCommerce\Admin\API\MarketingChannels',
'Automattic\WooCommerce\Admin\API\MarketingCampaigns',
'Automattic\WooCommerce\Admin\API\MarketingCampaignTypes',
'Automattic\WooCommerce\Admin\API\Options',
'Automattic\WooCommerce\Admin\API\PaymentGatewaySuggestions',
'Automattic\WooCommerce\Admin\API\Themes',
'Automattic\WooCommerce\Admin\API\Plugins',
'Automattic\WooCommerce\Admin\API\OnboardingFreeExtensions',
'Automattic\WooCommerce\Admin\API\OnboardingProductTypes',
'Automattic\WooCommerce\Admin\API\OnboardingProfile',
'Automattic\WooCommerce\Admin\API\OnboardingTasks',
'Automattic\WooCommerce\Admin\API\OnboardingThemes',
'Automattic\WooCommerce\Admin\API\OnboardingPlugins',
'Automattic\WooCommerce\Admin\API\OnboardingProducts',
'Automattic\WooCommerce\Admin\API\NavigationFavorites',
'Automattic\WooCommerce\Admin\API\MobileAppMagicLink',
'Automattic\WooCommerce\Admin\API\ShippingPartnerSuggestions',
);
}
if ( Features::is_enabled( 'launch-your-store' ) ) {
$controllers[] = 'Automattic\WooCommerce\Admin\API\LaunchYourStore';
}
if ( Features::is_enabled( 'analytics' ) ) {
$analytics_controllers = array(
'Automattic\WooCommerce\Admin\API\Customers',
'Automattic\WooCommerce\Admin\API\Leaderboards',
'Automattic\WooCommerce\Admin\API\Reports\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Import\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Export\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Products\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Variations\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Products\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Variations\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Revenue\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Orders\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Categories\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Taxes\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Coupons\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Stock\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Stock\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Downloads\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Customers\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\Controller',
if ( wc_rest_should_load_namespace( 'wc-analytics' ) ) {
// Controllers in wc-analytics namespace, but loaded irrespective of analytics feature value.
$analytic_mu_controllers = array(
'Automattic\WooCommerce\Admin\API\Notes',
'Automattic\WooCommerce\Admin\API\NoteActions',
'Automattic\WooCommerce\Admin\API\Coupons',
'Automattic\WooCommerce\Admin\API\Data',
'Automattic\WooCommerce\Admin\API\DataCountries',
'Automattic\WooCommerce\Admin\API\DataDownloadIPs',
'Automattic\WooCommerce\Admin\API\Orders',
'Automattic\WooCommerce\Admin\API\Products',
'Automattic\WooCommerce\Admin\API\ProductAttributes',
'Automattic\WooCommerce\Admin\API\ProductAttributeTerms',
'Automattic\WooCommerce\Admin\API\ProductCategories',
'Automattic\WooCommerce\Admin\API\ProductVariations',
'Automattic\WooCommerce\Admin\API\ProductReviews',
'Automattic\WooCommerce\Admin\API\ProductVariations',
'Automattic\WooCommerce\Admin\API\ProductsLowInStock',
'Automattic\WooCommerce\Admin\API\SettingOptions',
'Automattic\WooCommerce\Admin\API\Taxes',
);
// The performance indicators controller must be registered last, after other /stats endpoints have been registered.
$analytics_controllers[] = 'Automattic\WooCommerce\Admin\API\Reports\PerformanceIndicators\Controller';
$controllers = array_merge( $controllers, $analytics_controllers );
if ( Features::is_enabled( 'analytics' ) ) {
$analytics_controllers = array(
'Automattic\WooCommerce\Admin\API\Customers',
'Automattic\WooCommerce\Admin\API\Leaderboards',
'Automattic\WooCommerce\Admin\API\Reports\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Import\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Export\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Products\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Variations\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Products\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Variations\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Revenue\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Orders\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Categories\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Taxes\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Taxes\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Coupons\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Coupons\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Stock\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Stock\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Downloads\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Downloads\Stats\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Customers\Controller',
'Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\Controller',
);
// The performance indicators controllerq must be registered last, after other /stats endpoints have been registered.
$analytics_controllers[] = 'Automattic\WooCommerce\Admin\API\Reports\PerformanceIndicators\Controller';
$analytics_controllers = array_merge( $analytics_controllers, $analytic_mu_controllers );
}
$controllers = array_merge( $controllers, $analytics_controllers, $analytic_mu_controllers );
}
/**

View File

@@ -130,10 +130,12 @@ class Leaderboards extends \WC_REST_Data_Controller {
array(
'display' => wc_admin_number_format( $coupon['orders_count'] ),
'value' => $coupon['orders_count'],
'format' => 'number',
),
array(
'display' => wc_price( $coupon['amount'] ),
'value' => $coupon['amount'],
'format' => 'currency',
),
);
}
@@ -199,10 +201,12 @@ class Leaderboards extends \WC_REST_Data_Controller {
array(
'display' => wc_admin_number_format( $category['items_sold'] ),
'value' => $category['items_sold'],
'format' => 'number',
),
array(
'display' => wc_price( $category['net_revenue'] ),
'value' => $category['net_revenue'],
'format' => 'currency',
),
);
}
@@ -266,10 +270,12 @@ class Leaderboards extends \WC_REST_Data_Controller {
array(
'display' => wc_admin_number_format( $customer['orders_count'] ),
'value' => $customer['orders_count'],
'format' => 'number',
),
array(
'display' => wc_price( $customer['total_spend'] ),
'value' => $customer['total_spend'],
'format' => 'currency',
),
);
}
@@ -335,10 +341,12 @@ class Leaderboards extends \WC_REST_Data_Controller {
array(
'display' => wc_admin_number_format( $product['items_sold'] ),
'value' => $product['items_sold'],
'format' => 'number',
),
array(
'display' => wc_price( $product['net_revenue'] ),
'value' => $product['net_revenue'],
'format' => 'currency',
),
);
}
@@ -578,6 +586,14 @@ class Leaderboards extends \WC_REST_Data_Controller {
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'format' => array(
'description' => __( 'Table cell format.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'enum' => array( 'currency', 'number' ),
'readonly' => true,
'required' => false,
),
),
),
),

View File

@@ -97,37 +97,6 @@ class OnboardingFreeExtensions extends WC_REST_Data_Controller {
}
}
$extensions = $this->replace_jetpack_with_jetpack_boost_for_treatment( $extensions );
return new WP_REST_Response( $extensions );
}
private function replace_jetpack_with_jetpack_boost_for_treatment( array $extensions ) {
$is_treatment = \WooCommerce\Admin\Experimental_Abtest::in_treatment( 'woocommerce_jetpack_copy' );
if ( ! $is_treatment ) {
return $extensions;
}
$has_core_profiler = array_search( 'obw/core-profiler', array_column( $extensions, 'key' ) );
if ( $has_core_profiler === false ) {
return $extensions;
}
$has_jetpack = array_search( 'jetpack', array_column( $extensions[ $has_core_profiler ]['plugins'], 'key' ) );
if ( $has_jetpack === false ) {
return $extensions;
}
$jetpack = &$extensions[ $has_core_profiler ]['plugins'][ $has_jetpack ];
$jetpack->key = 'jetpack-boost';
$jetpack->name = 'Jetpack Boost';
$jetpack->label = __( 'Optimize store performance with Jetpack Boost', 'woocommerce' );
$jetpack->description = __( 'Speed up your store and improve your SEO with performance-boosting tools from Jetpack. Learn more', 'woocommerce' );
$jetpack->learn_more_link = 'https://jetpack.com/boost/';
return $extensions;
}
}

View File

@@ -202,6 +202,7 @@ class Options extends \WC_REST_Data_Controller {
'woocommerce_product_tour_modal_hidden',
'woocommerce_block_product_tour_shown',
'woocommerce_revenue_report_date_tour_shown',
'woocommerce_orders_report_date_tour_shown',
'woocommerce_show_prepublish_checks_enabled',
'woocommerce_date_type',
'date_format',

View File

@@ -239,7 +239,7 @@ class Orders extends \WC_REST_Orders_Controller {
}
// Format the order status.
$data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status'];
$data['status'] = OrderUtil::remove_status_prefix( $data['status'] );
// Format requested line items.
$formatted_line_items = array();

View File

@@ -140,17 +140,22 @@ class Controller extends GenericController {
* Returns the parent order number if the order is actually a refund.
*
* @param int $order_id Order ID.
* @return string
* @return string|null The Order Number or null if the order doesn't exist.
*/
protected function get_order_number( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order instanceof \WC_Order && ! $order instanceof \WC_Order_Refund ) {
if ( ! $this->is_valid_order( $order ) ) {
return null;
}
if ( 'shop_order_refund' === $order->get_type() ) {
$order = wc_get_order( $order->get_parent_id() );
// If the parent order doesn't exist, return null.
if ( ! $this->is_valid_order( $order ) ) {
return null;
}
}
if ( ! has_filter( 'woocommerce_order_number' ) ) {
@@ -160,22 +165,36 @@ class Controller extends GenericController {
return $order->get_order_number();
}
/**
* Whether the order is valid.
*
* @param bool|WC_Order|WC_Order_Refund $order Order object.
* @return bool True if the order is valid, false otherwise.
*/
protected function is_valid_order( $order ) {
return $order instanceof \WC_Order || $order instanceof \WC_Order_Refund;
}
/**
* Get the order total with the related currency formatting.
* Returns the parent order total if the order is actually a refund.
*
* @param int $order_id Order ID.
* @return string
* @return string|null The Order Number or null if the order doesn't exist.
*/
protected function get_total_formatted( $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order instanceof \WC_Order && ! $order instanceof \WC_Order_Refund ) {
if ( ! $this->is_valid_order( $order ) ) {
return null;
}
if ( 'shop_order_refund' === $order->get_type() ) {
$order = wc_get_order( $order->get_parent_id() );
if ( ! $this->is_valid_order( $order ) ) {
return null;
}
}
return wp_strip_all_tags( html_entity_decode( $order->get_formatted_order_total() ), true );

View File

@@ -524,14 +524,13 @@ class Controller extends ReportsController implements ExportableInterface {
$export_columns = array(
'date_created' => __( 'Date', 'woocommerce' ),
'order_number' => __( 'Order #', 'woocommerce' ),
'total_formatted' => __( 'N. Revenue (formatted)', 'woocommerce' ),
'status' => __( 'Status', 'woocommerce' ),
'customer_name' => __( 'Customer', 'woocommerce' ),
'customer_type' => __( 'Customer type', 'woocommerce' ),
'products' => __( 'Product(s)', 'woocommerce' ),
'num_items_sold' => __( 'Items sold', 'woocommerce' ),
'coupons' => __( 'Coupon(s)', 'woocommerce' ),
'net_total' => __( 'N. Revenue', 'woocommerce' ),
'net_total' => __( 'Net Sales', 'woocommerce' ),
'attribution' => __( 'Attribution', 'woocommerce' ),
);
@@ -555,16 +554,15 @@ class Controller extends ReportsController implements ExportableInterface {
*/
public function prepare_item_for_export( $item ) {
$export_item = array(
'date_created' => $item['date_created'],
'date_created' => $item['date'],
'order_number' => $item['order_number'],
'total_formatted' => $item['total_formatted'],
'status' => $item['status'],
'customer_name' => isset( $item['extended_info']['customer'] ) ? $this->get_customer_name( $item['extended_info']['customer'] ) : null,
'customer_type' => $item['customer_type'],
'products' => isset( $item['extended_info']['products'] ) ? $this->get_products( $item['extended_info']['products'] ) : null,
'num_items_sold' => $item['num_items_sold'],
'coupons' => isset( $item['extended_info']['coupons'] ) ? $this->get_coupons( $item['extended_info']['coupons'] ) : null,
'net_total' => $item['net_total'],
'net_total' => $item['net_total'],
'attribution' => $item['extended_info']['attribution']['origin'],
);

View File

@@ -82,7 +82,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
'total_tax' => 'SUM(total_tax) as total_tax',
'order_tax' => 'SUM(order_tax) as order_tax',
'shipping_tax' => 'SUM(shipping_tax) as shipping_tax',
'orders_count' => "COUNT({$table_name}.order_id) as orders_count",
'orders_count' => "COUNT( DISTINCT ( CASE WHEN parent_id = 0 THEN {$table_name}.order_id END ) ) as orders_count",
);
}
@@ -146,8 +146,6 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
public function get_data( $query_args ) {
global $wpdb;
$table_name = self::get_db_table_name();
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
@@ -204,6 +202,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$this->subquery->add_sql_clause( 'select', $this->selected_columns( $query_args ) );
$this->subquery->add_sql_clause( 'group_by', ", {$wpdb->prefix}woocommerce_order_items.order_item_name, {$wpdb->prefix}woocommerce_order_itemmeta.meta_value" );
$this->subquery->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->subquery->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
$taxes_query = $this->subquery->get_query_statement();

View File

@@ -39,6 +39,7 @@ class Controller extends ReportsController implements ExportableInterface {
*/
protected $param_mapping = array(
'variations' => 'variation_includes',
'products' => 'product_includes',
);
/**
@@ -382,6 +383,15 @@ class Controller extends ReportsController implements ExportableInterface {
'sanitize_callback' => 'wp_validate_boolean',
'validate_callback' => 'rest_validate_request_arg',
);
$params['products'] = array(
'description' => __( 'Limit result to items with specified product ids.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
return $params;
}

View File

@@ -179,10 +179,8 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
if ( $included_variations ) {
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id IN ({$included_variations})" );
} elseif ( ! $included_products ) {
if ( $this->should_exclude_simple_products( $query_args ) ) {
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id != 0" );
}
} elseif ( $this->should_exclude_simple_products( $query_args ) ) {
$this->subquery->add_sql_clause( 'where', "AND {$order_product_lookup_table}.variation_id != 0" );
}
$order_status_filter = $this->get_status_subquery( $query_args );

View File

@@ -56,7 +56,7 @@ class DefaultMarketingRecommendations {
return array(
array(
'title' => 'Google Listings and Ads',
'title' => 'Google for WooCommerce',
'description' => __( 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', 'woocommerce' ),
'url' => "https://woocommerce.com/products/google-listings-and-ads/{$utm_string}",
'direct_install' => true,

View File

@@ -165,7 +165,7 @@ class TaskLists {
),
),
'tasks' => array(
'StoreConnect',
'ExtendStore',
'AdditionalPayments',
'GetMobileApp',
),

View File

@@ -3,7 +3,7 @@
namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Jetpack_Gutenberg;
use WP_Post;
/**
* Customize Your Store Task
@@ -24,23 +24,24 @@ class CustomizeStore extends Task {
// Hook to remove unwanted UI elements when users are viewing with ?cys-hide-admin-bar=true.
add_action( 'wp_head', array( $this, 'possibly_remove_unwanted_ui_elements' ) );
add_action( 'save_post_wp_global_styles', array( $this, 'mark_task_as_complete' ), 10, 3 );
add_action( 'save_post_wp_template', array( $this, 'mark_task_as_complete' ), 10, 3 );
add_action( 'save_post_wp_template_part', array( $this, 'mark_task_as_complete' ), 10, 3 );
add_action( 'save_post_wp_global_styles', array( $this, 'mark_task_as_complete_block_theme' ), 10, 3 );
add_action( 'save_post_wp_template', array( $this, 'mark_task_as_complete_block_theme' ), 10, 3 );
add_action( 'save_post_wp_template_part', array( $this, 'mark_task_as_complete_block_theme' ), 10, 3 );
add_action( 'customize_save_after', array( $this, 'mark_task_as_complete_classic_theme' ) );
}
/**
* Mark the CYS task as complete whenever the user updates their global styles.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
* @param bool $update Whether this is an existing post being updated.
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
* @param bool $update Whether this is an existing post being updated.
*
* @return void
*/
public function mark_task_as_complete( $post_id, $post, $update ) {
if ( $post instanceof \WP_Post ) {
$is_cys_complete = '{"version": 2, "isGlobalStylesUserThemeJSON": true }' !== $post->post_content || in_array( $post->post_type, array( 'wp_template', 'wp_template_part' ), true );
public function mark_task_as_complete_block_theme( $post_id, $post, $update ) {
if ( $post instanceof WP_Post ) {
$is_cys_complete = $this->has_custom_global_styles( $post ) || $this->has_custom_template( $post );
if ( $is_cys_complete ) {
update_option( 'woocommerce_admin_customize_store_completed', 'yes' );
@@ -48,6 +49,15 @@ class CustomizeStore extends Task {
}
}
/**
* Mark the CYS task as complete whenever the user saves the customizer changes.
*
* @return void
*/
public function mark_task_as_complete_classic_theme() {
update_option( 'woocommerce_admin_customize_store_completed', 'yes' );
}
/**
* ID.
*
@@ -227,11 +237,6 @@ class CustomizeStore extends Task {
* @since 8.0.3
*/
do_action( 'enqueue_block_editor_assets' );
// Load Jetpack's block editor assets because they are not enqueued by default.
if ( class_exists( 'Jetpack_Gutenberg' ) ) {
Jetpack_Gutenberg::enqueue_block_editor_assets();
}
}
/**
@@ -260,4 +265,33 @@ class CustomizeStore extends Task {
</style>';
}
}
/**
* Checks if the post has custom global styles stored (if it is different from the default global styles).
*
* @param WP_Post $post The post object.
* @return bool
*/
private function has_custom_global_styles( WP_Post $post ) {
$required_keys = array( 'version', 'isGlobalStylesUserThemeJSON' );
$json_post_content = json_decode( $post->post_content, true );
if ( is_null( $json_post_content ) ) {
return false;
}
$post_content_keys = array_keys( $json_post_content );
return ! empty( array_diff( $post_content_keys, $required_keys ) ) || ! empty( array_diff( $required_keys, $post_content_keys ) );
}
/**
* Checks if the post is a template or a template part.
*
* @param WP_Post $post The post object.
* @return bool Whether the post is a template or a template part.
*/
private function has_custom_template( WP_Post $post ) {
return in_array( $post->post_type, array( 'wp_template', 'wp_template_part' ), true );
}
}

View File

@@ -62,7 +62,9 @@ class ExperimentalShippingRecommendation extends Task {
* @return bool
*/
public function can_view() {
return Features::is_enabled( 'shipping-smart-defaults' );
return Features::is_enabled( 'shipping-smart-defaults' ) &&
! PluginsHelper::is_plugin_active( 'woocommerce-shipping' ) &&
! PluginsHelper::is_plugin_active( 'woocommerce-tax' );
}
/**

View File

@@ -5,16 +5,16 @@ namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
/**
* Connect store to WooCommerce.com Task
* ExtendStore Task
*/
class StoreConnect extends Task {
class ExtendStore extends Task {
/**
* ID.
*
* @return string
*/
public function get_id() {
return 'connect-store';
return 'extend-store';
}
/**
@@ -23,7 +23,7 @@ class StoreConnect extends Task {
* @return string
*/
public function get_title() {
return __( 'Manage your WooCommerce.com Marketplace subscriptions', 'woocommerce' );
return __( 'Enhance your store with extensions', 'woocommerce' );
}
/**
@@ -50,7 +50,7 @@ class StoreConnect extends Task {
* @return bool
*/
public function is_complete() {
return \WC_Helper::is_site_connected();
return $this->is_visited();
}
/**
@@ -59,7 +59,7 @@ class StoreConnect extends Task {
* @return bool
*/
public function is_dismissable() {
return true;
return false;
}
/**
@@ -68,6 +68,6 @@ class StoreConnect extends Task {
* @return string
*/
public function get_action_url() {
return admin_url( 'admin.php?page=wc-admin&tab=my-subscriptions&path=/extensions' );
return admin_url( 'admin.php?page=wc-admin&path=/extensions' );
}
}

View File

@@ -77,11 +77,12 @@ class Payments extends Task {
*/
public function can_view() {
$woocommerce_payments = $this->task_list->get_task( 'woocommerce-payments' );
// Make sure the task is mutually exclusive with the WooPayments task.
return Features::is_enabled( 'payment-gateway-suggestions' ) && ! $woocommerce_payments->can_view();
}
/**
* Check if the store has any enabled gateways.
* Check if the store has any enabled gateways, other than WooPayments.
*
* @return bool
*/
@@ -90,7 +91,8 @@ class Payments extends Task {
$enabled_gateways = array_filter(
$gateways,
function( $gateway ) {
return 'yes' === $gateway->enabled && 'woocommerce_payments' !== $gateway->id;
// Filter out any WooPayments gateways as this task is mutually exclusive with the WooPayments task.
return 'yes' === $gateway->enabled && 0 !== strpos( $gateway->id, 'woocommerce_payments' );
}
);

View File

@@ -127,9 +127,11 @@ class Tax extends Task {
*/
public function get_additional_data() {
return array(
'avalara_activated' => PluginsHelper::is_plugin_active( 'woocommerce-avatax' ),
'tax_jar_activated' => class_exists( 'WC_Taxjar' ),
'woocommerce_tax_countries' => self::get_automated_support_countries(),
'avalara_activated' => PluginsHelper::is_plugin_active( 'woocommerce-avatax' ),
'tax_jar_activated' => class_exists( 'WC_Taxjar' ),
'woocommerce_tax_activated' => PluginsHelper::is_plugin_active( 'woocommerce-tax' ),
'woocommerce_shipping_activated' => PluginsHelper::is_plugin_active( 'woocommerce-shipping' ),
'woocommerce_tax_countries' => self::get_automated_support_countries(),
);
}

View File

@@ -103,8 +103,7 @@ class WooCommercePayments extends Task {
public function can_view() {
$payments = $this->task_list->get_task( 'payments' );
return ! $payments->is_complete() && // Do not re-display the task if the "add payments" task has already been completed.
self::is_installed() &&
return ! $payments->is_complete() && // Do not re-display the task if the general "Payments" task has already been completed.
self::is_supported();
}
@@ -175,7 +174,7 @@ class WooCommercePayments extends Task {
}
/**
* Check if the store is in a supported country.
* Check if the store is in a WooPayments supported country.
*
* @return bool
*/

View File

@@ -65,6 +65,13 @@ class DefaultPaymentGateways {
'CA',
)
),
(object) array(
'type' => 'or',
'operands' => array(
self::get_rules_for_wcpay_activated( false ),
self::get_rules_for_wcpay_connected( false ),
),
),
),
'category_other' => array(),
'category_additional' => array(
@@ -87,6 +94,13 @@ class DefaultPaymentGateways {
'AU',
)
),
(object) array(
'type' => 'or',
'operands' => array(
self::get_rules_for_wcpay_activated( false ),
self::get_rules_for_wcpay_connected( false ),
),
),
),
'category_other' => array(),
'category_additional' => array(
@@ -250,6 +264,19 @@ class DefaultPaymentGateways {
)
),
self::get_rules_for_cbd( false ),
(object) array(
'type' => 'or',
'operands' => array(
(object) array(
'type' => 'not',
'operand' => array(
self::get_rules_for_countries( self::get_wcpay_countries() ),
),
),
self::get_rules_for_wcpay_activated( false ),
self::get_rules_for_wcpay_connected( false ),
),
),
),
'category_other' => array(),
'category_additional' => array(
@@ -862,6 +889,47 @@ class DefaultPaymentGateways {
),
),
),
array(
'id' => 'woocommerce_payments:bnpl',
'title' => __( 'Activate BNPL instantly on WooPayments', 'woocommerce' ),
'content' => __(
'The worlds favorite buy now, pay later options and many more are right at your fingertips with WooPayments — all from one dashboard, without needing multiple extensions and logins.',
'woocommerce'
),
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay-bnpl.svg',
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/wcpay-bnpl.svg',
'plugins' => array( 'woocommerce-payments' ),
'is_visible' => array(
self::get_rules_for_countries(
array_intersect(
array(
'US',
'CA',
'AU',
'AT',
'BE',
'CH',
'DK',
'ES',
'FI',
'FR',
'DE',
'GB',
'IT',
'NL',
'NO',
'PL',
'SE',
'NZ',
),
self::get_wcpay_countries()
),
),
self::get_rules_for_cbd( false ),
self::get_rules_for_wcpay_activated( true ),
self::get_rules_for_wcpay_connected( true ),
),
),
array(
'id' => 'zipmoney',
'title' => __( 'Zip Co - Buy Now, Pay Later', 'woocommerce' ),
@@ -976,7 +1044,7 @@ class DefaultPaymentGateways {
* Get default rules for CBD based on given argument.
*
* @param bool $should_have Whether or not the store should have CBD as an industry (true) or not (false).
* @return array Rules to match.
* @return object Rules to match.
*/
public static function get_rules_for_cbd( $should_have ) {
return (object) array(
@@ -1002,6 +1070,62 @@ class DefaultPaymentGateways {
);
}
/**
* Get default rules for the WooPayments plugin being installed and activated.
*
* @param bool $should_be Whether WooPayments should be activated.
*
* @return object Rules to match.
*/
public static function get_rules_for_wcpay_activated( $should_be ) {
$active_rule = (object) array(
'type' => 'plugins_activated',
'plugins' => array( 'woocommerce-payments' ),
);
if ( $should_be ) {
return $active_rule;
}
return (object) array(
'type' => 'not',
'operand' => array( $active_rule ),
);
}
/**
* Get default rules for WooPayments being connected or not.
*
* This does not include the check for the WooPayments plugin to be active.
*
* @param bool $should_be Whether WooPayments should be connected.
*
* @return object Rules to match.
*/
public static function get_rules_for_wcpay_connected( $should_be ) {
return (object) array(
'type' => 'option',
'transformers' => array(
// Extract only the 'data' key from the option.
(object) array(
'use' => 'dot_notation',
'arguments' => (object) array(
'path' => 'data',
),
),
// Extract the keys from the data array.
(object) array(
'use' => 'array_keys',
),
),
'option_name' => 'wcpay_account_data',
// The rule will be look for the 'account_id' key in the account data array.
'operation' => $should_be ? 'contains' : '!contains',
'value' => 'account_id',
'default' => array(),
);
}
/**
* Get recommendation priority for a given payment gateway by id and country.
* If country is not supported, return null.
@@ -1013,6 +1137,8 @@ class DefaultPaymentGateways {
private static function get_recommendation_priority( $gateway_id, $country_code ) {
$recommendation_priority_map = array(
'US' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1023,6 +1149,8 @@ class DefaultPaymentGateways {
'klarna_payments',
),
'CA' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1032,6 +1160,8 @@ class DefaultPaymentGateways {
'klarna_payments',
),
'AT' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1041,6 +1171,8 @@ class DefaultPaymentGateways {
'amazon_payments_advanced',
),
'BE' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1049,26 +1181,63 @@ class DefaultPaymentGateways {
'klarna_payments',
'amazon_payments_advanced',
),
'BG' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ),
'HR' => array( 'woocommerce_payments', 'ppcp-gateway' ),
'BG' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
),
'HR' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'ppcp-gateway',
),
'CH' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
'mollie_wc_gateway_banktransfer',
'klarna_payments',
),
'CY' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ),
'CZ' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ),
'CY' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
'amazon_payments_advanced',
),
'CZ' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
),
'DK' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
'klarna_payments',
'amazon_payments_advanced',
),
'EE' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'airwallex_main' ),
'EE' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
'airwallex_main',
),
'ES' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1078,6 +1247,8 @@ class DefaultPaymentGateways {
'amazon_payments_advanced',
),
'FI' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1086,6 +1257,8 @@ class DefaultPaymentGateways {
'klarna_payments',
),
'FR' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1096,6 +1269,8 @@ class DefaultPaymentGateways {
'amazon_payments_advanced',
),
'DE' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1105,6 +1280,8 @@ class DefaultPaymentGateways {
'amazon_payments_advanced',
),
'GB' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1114,9 +1291,25 @@ class DefaultPaymentGateways {
'klarna_payments',
'amazon_payments_advanced',
),
'GR' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'airwallex_main' ),
'HU' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ),
'GR' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
'airwallex_main',
),
'HU' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
'amazon_payments_advanced',
),
'IE' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1125,6 +1318,8 @@ class DefaultPaymentGateways {
'amazon_payments_advanced',
),
'IT' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1133,11 +1328,38 @@ class DefaultPaymentGateways {
'klarna_payments',
'amazon_payments_advanced',
),
'LV' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ),
'LT' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ),
'LU' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ),
'MT' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ),
'LV' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
),
'LT' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
),
'LU' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
'amazon_payments_advanced',
),
'MT' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
),
'NL' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1146,8 +1368,18 @@ class DefaultPaymentGateways {
'klarna_payments',
'amazon_payments_advanced',
),
'NO' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'kco', 'klarna_payments' ),
'NO' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
'kco',
'klarna_payments',
),
'PL' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1156,16 +1388,39 @@ class DefaultPaymentGateways {
'klarna_payments',
),
'PT' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
'airwallex_main',
'amazon_payments_advanced',
),
'RO' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ),
'SK' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway' ),
'SL' => array( 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ),
'RO' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
),
'SK' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
),
'SL' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
'amazon_payments_advanced',
),
'SE' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
@@ -1194,6 +1449,8 @@ class DefaultPaymentGateways {
'UY' => array( 'woo-mercado-pago-custom', 'ppcp-gateway' ),
'VE' => array( 'ppcp-gateway' ),
'AU' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'airwallex_main',
@@ -1203,6 +1460,8 @@ class DefaultPaymentGateways {
'klarna_payments',
),
'NZ' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'airwallex_main',
@@ -1210,6 +1469,8 @@ class DefaultPaymentGateways {
'klarna_payments',
),
'HK' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'airwallex_main',
@@ -1217,13 +1478,22 @@ class DefaultPaymentGateways {
'payoneer-checkout',
),
'JP' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'ppcp-gateway',
'square_credit_card',
'amazon_payments_advanced',
),
'SG' => array( 'woocommerce_payments', 'stripe', 'airwallex_main', 'ppcp-gateway' ),
'SG' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
'stripe',
'airwallex_main',
'ppcp-gateway',
),
'CN' => array( 'airwallex_main', 'ppcp-gateway', 'payoneer-checkout' ),
'FJ' => array(),
'GU' => array(),
@@ -1232,7 +1502,11 @@ class DefaultPaymentGateways {
'ZA' => array( 'payfast', 'paystack' ),
'NG' => array( 'paystack' ),
'GH' => array( 'paystack' ),
'AE' => array( 'woocommerce_payments' ),
'AE' => array(
'woocommerce_payments:with-in-person-payments',
'woocommerce_payments:without-in-person-payments',
'woocommerce_payments',
),
);
// If the country code is not in the list, return default priority.

View File

@@ -16,15 +16,33 @@ class EvaluateSuggestion {
/**
* Evaluates the spec and returns the suggestion.
*
* @param object|array $spec The suggestion to evaluate.
* @param object|array $spec The suggestion to evaluate.
* @param array $logger_args Optional. Arguments for the rule evaluator logger.
*
* @return object The evaluated suggestion.
*/
public static function evaluate( $spec ) {
public static function evaluate( $spec, $logger_args = array() ) {
$rule_evaluator = new RuleEvaluator();
$suggestion = is_array( $spec ) ? (object) $spec : clone $spec;
if ( isset( $suggestion->is_visible ) ) {
$is_visible = $rule_evaluator->evaluate( $suggestion->is_visible );
// Determine the suggestion's logger slug.
$logger_slug = ! empty( $suggestion->id ) ? $suggestion->id : '';
// If the suggestion has no ID, use the title to generate a slug.
if ( empty( $logger_slug ) ) {
$logger_slug = ! empty( $suggestion->title ) ? sanitize_title_with_dashes( trim( $suggestion->title ) ) : 'anonymous-suggestion';
}
// Evaluate the visibility of the suggestion.
$is_visible = $rule_evaluator->evaluate(
$suggestion->is_visible,
null,
array(
'slug' => $logger_slug,
'source' => $logger_args['source'] ?? 'wc-payment-gateway-suggestions',
)
);
$suggestion->is_visible = $is_visible;
}
@@ -35,15 +53,17 @@ class EvaluateSuggestion {
* Evaluates the specs and returns the visible suggestions.
*
* @param array $specs payment suggestion spec array.
* @param array $logger_args Optional. Arguments for the rule evaluator logger.
*
* @return array The visible suggestions and errors.
*/
public static function evaluate_specs( $specs ) {
public static function evaluate_specs( $specs, $logger_args = array() ) {
$suggestions = array();
$errors = array();
foreach ( $specs as $spec ) {
try {
$suggestion = self::evaluate( $spec );
$suggestion = self::evaluate( $spec, $logger_args );
if ( ! property_exists( $suggestion, 'is_visible' ) || $suggestion->is_visible ) {
$suggestions[] = $suggestion;
}

View File

@@ -127,7 +127,7 @@ class Init {
$editor_settings = $this->get_product_editor_settings();
$script_handle = 'wc-admin-edit-product';
wp_register_script( $script_handle, '', array(), '0.1.0', true );
wp_register_script( $script_handle, '', array( 'wp-blocks' ), '0.1.0', true );
wp_enqueue_script( $script_handle );
wp_add_inline_script(
$script_handle,
@@ -150,9 +150,10 @@ class Init {
* Enqueue styles needed for the rich text editor.
*/
public function enqueue_styles() {
if ( ! PageController::is_admin_or_embed_page() ) {
if ( ! PageController::is_admin_page() ) {
return;
}
wp_enqueue_style( 'wc-product-editor' );
wp_enqueue_style( 'wp-edit-blocks' );
wp_enqueue_style( 'wp-format-library' );
wp_enqueue_editor();
@@ -168,7 +169,7 @@ class Init {
* Dequeue conflicting styles.
*/
public function dequeue_conflicting_styles() {
if ( ! PageController::is_admin_or_embed_page() ) {
if ( ! PageController::is_admin_page() ) {
return;
}
// Dequeing this to avoid conflicts, until we remove the 'woocommerce-page' class.

View File

@@ -285,6 +285,24 @@ class DefaultShippingPartners {
'learn_more_link' => 'https://woocommerce.com/products/shipping/',
'is_visible' => array(
self::get_rules_for_countries( array( 'US' ) ),
(object) array(
'type' => 'not',
'operand' => array(
(object) array(
'type' => 'plugins_activated',
'plugins' => array( 'woocommerce-shipping' ),
),
),
),
(object) array(
'type' => 'not',
'operand' => array(
(object) array(
'type' => 'plugins_activated',
'plugins' => array( 'woocommerce-tax' ),
),
),
),
),
'available_layouts' => array( 'column' ),
),

View File

@@ -20,14 +20,14 @@ class ShippingPartnerSuggestions extends RemoteSpecsEngine {
$locale = get_user_locale();
$specs = is_array( $specs ) ? $specs : self::get_specs();
$results = EvaluateSuggestion::evaluate_specs( $specs );
$results = EvaluateSuggestion::evaluate_specs( $specs, array( 'source' => 'wc-shipping-partner-suggestions' ) );
$specs_to_return = $results['suggestions'];
$specs_to_save = null;
if ( empty( $specs_to_return ) ) {
// When suggestions is empty, replace it with defaults and save for 3 hours.
$specs_to_save = DefaultShippingPartners::get_all();
$specs_to_return = EvaluateSuggestion::evaluate_specs( $specs_to_save )['suggestions'];
$specs_to_return = EvaluateSuggestion::evaluate_specs( $specs_to_save, array( 'source' => 'wc-shipping-partner-suggestions' ) )['suggestions'];
} elseif ( count( $results['errors'] ) > 0 ) {
// When suggestions is not empty but has errors, save it for 3 hours.
$specs_to_save = $specs;

View File

@@ -423,7 +423,7 @@ class Notes {
unset( $_COOKIE['tk_ai'] );
wc_admin_record_tracks_event( $event_name, $params );
if ( isset( $anon_id ) ) {
setcookie( 'tk_ai', $anon_id );
WC_Site_Tracking::set_tracking_cookie( 'tk_ai', $anon_id );
}
}

View File

@@ -43,6 +43,11 @@ class PluginsHelper {
*/
const WOO_SUBSCRIPTION_PAGE_URL = 'https://woocommerce.com/my-account/my-subscriptions/';
/**
* The URL for the WooCommerce.com add payment method page.
*/
const WOO_ADD_PAYMENT_METHOD_URL = 'https://woocommerce.com/my-account/add-payment-method/';
/**
* Meta key for dismissing expired subscription notices.
*/
@@ -588,9 +593,11 @@ class PluginsHelper {
$connect_page_url = add_query_arg(
array(
'page' => 'wc-admin',
'tab' => 'my-subscriptions',
'path' => rawurlencode( '/extensions' ),
'page' => 'wc-admin',
'tab' => 'my-subscriptions',
'path' => rawurlencode( '/extensions' ),
'utm_source' => 'pu',
'utm_campaign' => 'pu_setting_screen_connect',
),
admin_url( 'admin.php' )
);
@@ -706,7 +713,7 @@ class PluginsHelper {
}
/**
* Construct the subscritpion notice data based on user subscriptions data.
* Construct the subscription notice data based on user subscriptions data.
*
* @param array $all_subs all subscription data.
* @param array $subs_to_show filtered subscriptions as condition.
@@ -717,10 +724,19 @@ class PluginsHelper {
*/
public static function get_subscriptions_notice_data( array $all_subs, array $subs_to_show, int $total, array $messages, string $type ) {
if ( 1 < $total ) {
$hyperlink_url = add_query_arg(
array(
'utm_source' => 'pu',
'utm_campaign' => 'expired' === $type ? 'pu_settings_screen_renew' : 'pu_settings_screen_enable_autorenew',
),
self::WOO_SUBSCRIPTION_PAGE_URL
);
$parsed_message = sprintf(
$messages['different_subscriptions'],
esc_attr( $total ),
esc_url( self::WOO_SUBSCRIPTION_PAGE_URL ),
esc_url( $hyperlink_url ),
esc_attr( $total ),
);
@@ -752,8 +768,11 @@ class PluginsHelper {
$expiry_date = date_i18n( 'F jS', $subscription['expires'] );
$hyperlink_url = add_query_arg(
array(
'product_id' => $product_id,
'type' => $type,
'product_id' => $product_id,
'type' => $type,
'utm_source' => 'pu',
'utm_campaign' => 'expired' === $type ? 'pu_settings_screen_renew' : 'pu_settings_screen_enable_autorenew',
),
self::WOO_SUBSCRIPTION_PAGE_URL
);
@@ -810,6 +829,7 @@ class PluginsHelper {
$subscriptions,
function ( $sub ) {
return ( ! empty( $sub['local']['installed'] ) && ! empty( $sub['product_key'] ) )
&& $sub['active']
&& $sub['expiring']
&& ! $sub['autorenew'];
},
@@ -824,29 +844,7 @@ class PluginsHelper {
// When payment method is missing on WooCommerce.com.
$helper_notices = WC_Helper::get_notices();
if ( ! empty( $helper_notices['missing_payment_method_notice'] ) ) {
$description = $allowed_link
? sprintf(
/* translators: %s: WooCommerce.com URL to add payment method */
_n(
'Your WooCommerce extension subscription is missing a payment method for renewal. <a href="%s">Add a payment method</a> to ensure you continue receiving updates and streamlined support.',
'Your WooCommerce extension subscriptions are missing a payment method for renewal. <a href="%s">Add a payment method</a> to ensure you continue receiving updates and streamlined support.',
$total_expiring_subscriptions,
'woocommerce'
),
'https://woocommerce.com/my-account/add-payment-method/'
)
: _n(
'Your WooCommerce extension subscription is missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.',
'Your WooCommerce extension subscriptions are missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.',
$total_expiring_subscriptions,
'woocommerce'
);
return array(
'description' => $description,
'button_text' => __( 'Add payment method', 'woocommerce' ),
'button_link' => 'https://woocommerce.com/my-account/add-payment-method/',
);
return self::get_missing_payment_method_notice( $allowed_link, $total_expiring_subscriptions );
}
// Payment method is available but there are expiring subscriptions.
@@ -865,14 +863,20 @@ class PluginsHelper {
'expiring',
);
$button_link = self::WOO_SUBSCRIPTION_PAGE_URL;
$button_link = add_query_arg(
array(
'utm_source' => 'pu',
'utm_campaign' => 'pu_in_apps_screen_enable_autorenew',
),
self::WOO_SUBSCRIPTION_PAGE_URL
);
if ( in_array( $notice_data['type'], array( 'single_manage', 'multiple_manage' ), true ) ) {
$button_link = add_query_arg(
array(
'product_id' => $notice_data['product_id'],
'type' => 'expiring',
),
self::WOO_SUBSCRIPTION_PAGE_URL
$button_link
);
}
@@ -903,6 +907,7 @@ class PluginsHelper {
$subscriptions,
function ( $sub ) {
return ( ! empty( $sub['local']['installed'] ) && ! empty( $sub['product_key'] ) )
&& $sub['active']
&& $sub['expired']
&& ! $sub['lifetime'];
},
@@ -923,21 +928,28 @@ class PluginsHelper {
/* translators: 1) product name 3) URL to My Subscriptions page 4) Renew product price string */
'single_manage' => __( 'Your subscription for <strong>%1$s</strong> expired. <a href="%3$s">%4$s</a> to continue receiving updates and streamlined support.', 'woocommerce' ),
/* translators: 1) product name 3) URL to My Subscriptions page 4) Renew product price string */
'multiple_manage' => __( 'One of your subscriptions for <strong>%1$s</strong> has expired. <a href="%3$s">%4$s.</a> to continue receiving updates and streamlined support.', 'woocommerce' ),
'multiple_manage' => __( 'One of your subscriptions for <strong>%1$s</strong> has expired. <a href="%3$s">%4$s</a> to continue receiving updates and streamlined support.', 'woocommerce' ),
/* translators: 1) total expired subscriptions 2) URL to My Subscriptions page */
'different_subscriptions' => __( 'You have <strong>%1$s Woo extension subscriptions</strong> that expired. <a href="%2$s">Renew</a> to continue receiving updates and streamlined support.', 'woocommerce' ),
),
'expired',
);
$button_link = self::WOO_SUBSCRIPTION_PAGE_URL;
$button_link = add_query_arg(
array(
'utm_source' => 'pu',
'utm_campaign' => $allowed_link ? 'pu_settings_screen_renew' : 'pu_in_apps_screen_renew',
),
self::WOO_SUBSCRIPTION_PAGE_URL
);
if ( in_array( $notice_data['type'], array( 'single_manage', 'multiple_manage' ), true ) ) {
$button_link = add_query_arg(
array(
'product_id' => $notice_data['product_id'],
'type' => 'expiring',
),
self::WOO_SUBSCRIPTION_PAGE_URL
$button_link
);
}
@@ -973,4 +985,45 @@ class PluginsHelper {
return true;
}
/**
* Get the notice data for missing payment method.
*
* @param bool $allowed_link whether should show link on the notice or not.
* @param int $total_expiring_subscriptions total expiring subscriptions.
*
* @return array the notices data.
*/
public static function get_missing_payment_method_notice( $allowed_link = true, $total_expiring_subscriptions = 1 ) {
$add_payment_method_link = add_query_arg(
array(
'utm_source' => 'pu',
'utm_campaign' => $allowed_link ? 'pu_settings_screen_add_payment_method' : 'pu_in_apps_screen_add_payment_method',
),
self::WOO_ADD_PAYMENT_METHOD_URL
);
$description = $allowed_link
? sprintf(
/* translators: %s: WooCommerce.com URL to add payment method */
_n(
'Your WooCommerce extension subscription is missing a payment method for renewal. <a href="%s">Add a payment method</a> to ensure you continue receiving updates and streamlined support.',
'Your WooCommerce extension subscriptions are missing a payment method for renewal. <a href="%s">Add a payment method</a> to ensure you continue receiving updates and streamlined support.',
$total_expiring_subscriptions,
'woocommerce'
),
$add_payment_method_link
)
: _n(
'Your WooCommerce extension subscription is missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.',
'Your WooCommerce extension subscriptions are missing a payment method for renewal. Add a payment method to ensure you continue receiving updates and streamlined support.',
$total_expiring_subscriptions,
'woocommerce'
);
return array(
'description' => $description,
'button_text' => __( 'Add payment method', 'woocommerce' ),
'button_link' => $add_payment_method_link,
);
}
}

View File

@@ -7,11 +7,14 @@ namespace Automattic\WooCommerce\Admin\RemoteInboxNotifications;
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Notes\Notes;
use Automattic\WooCommerce\Admin\PluginsProvider\PluginsProvider;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine;
use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\StoredStateSetupForProducts;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
/**
* Remote Inbox Notifications engine.
@@ -19,11 +22,15 @@ use Automattic\WooCommerce\Admin\RemoteSpecs\RuleProcessors\StoredStateSetupForP
* specs that are able to be triggered.
*/
class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
use AccessiblePrivateMethods;
const STORED_STATE_OPTION_NAME = 'wc_remote_inbox_notifications_stored_state';
const WCA_UPDATED_OPTION_NAME = 'wc_remote_inbox_notifications_wca_updated';
/**
* Initialize the engine.
* phpcs:disable WooCommerce.Functions.InternalInjectionMethod.MissingFinal
* phpcs:disable WooCommerce.Functions.InternalInjectionMethod.MissingInternalTag
*/
public static function init() {
// Init things that need to happen before admin_init.
@@ -42,10 +49,13 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
// Hook into WCA updated. This is hooked up here rather than in
// on_admin_init because that runs too late to hook into the action.
add_action( 'woocommerce_run_on_woocommerce_admin_updated', array( __CLASS__, 'run_on_woocommerce_admin_updated' ) );
add_action(
'woocommerce_run_on_woocommerce_admin_updated',
array( __CLASS__, 'run_on_woocommerce_admin_updated' )
);
add_action(
'woocommerce_updated',
function() {
function () {
$next_hook = WC()->queue()->get_next(
'woocommerce_run_on_woocommerce_admin_updated',
array(),
@@ -63,6 +73,11 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
);
add_filter( 'woocommerce_get_note_from_db', array( __CLASS__, 'get_note_from_db' ), 10, 1 );
self::add_filter( 'woocommerce_debug_tools', array( __CLASS__, 'add_debug_tools' ) );
self::add_action(
'wp_ajax_woocommerce_json_inbox_notifications_search',
array( __CLASS__, 'ajax_action_inbox_notification_search' )
);
}
/**
@@ -155,7 +170,7 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
public static function get_stored_state() {
$stored_state = get_option( self::STORED_STATE_OPTION_NAME );
if ( $stored_state === false ) {
if ( false === $stored_state ) {
$stored_state = new \stdClass();
$stored_state = StoredStateSetupForProducts::init_stored_state(
@@ -199,6 +214,7 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
* Get the note. This is used to display localized note.
*
* @param Note $note_from_db The note object created from db.
*
* @return Note The note.
*/
public static function get_note_from_db( $note_from_db ) {
@@ -211,7 +227,7 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
continue;
}
$locale = SpecRunner::get_locale( $spec->locales, true );
if ( $locale === null ) {
if ( null === $locale ) {
// No locale found, so don't update the note.
break;
}
@@ -233,4 +249,94 @@ class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
return $note_from_db;
}
/**
* Add the debug tools to the WooCommerce debug tools (WooCommerce > Status > Tools).
*
* @param array $tools a list of tools.
*
* @return mixed
*/
private static function add_debug_tools( $tools ) {
// Check if the feature flag is disabled.
if ( ! Features::is_enabled( 'remote-inbox-notifications' ) ) {
return false;
}
// Check if the site has opted out of marketplace suggestions.
if ( get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) !== 'yes' ) {
return false;
}
$tools['refresh_remote_inbox_notifications'] = array(
'name' => __( 'Refresh Remote Inbox Notifications', 'woocommerce' ),
'button' => __( 'Refresh', 'woocommerce' ),
'desc' => __( 'This will refresh the remote inbox notifications', 'woocommerce' ),
'callback' => function () {
RemoteInboxNotificationsDataSourcePoller::get_instance()->read_specs_from_data_sources();
RemoteInboxNotificationsEngine::run();
return __( 'Remote inbox notifications have been refreshed', 'woocommerce' );
},
);
$tools['delete_inbox_notification'] = array(
'name' => __( 'Delete an Inbox Notification', 'woocommerce' ),
'button' => __( 'Delete', 'woocommerce' ),
'desc' => __( 'This will delete an inbox notification by slug', 'woocommerce' ),
'selector' => array(
'description' => __( 'Select an inbox notification to delete:', 'woocommerce' ),
'class' => 'wc-product-search',
'search_action' => 'woocommerce_json_inbox_notifications_search',
'name' => 'delete_inbox_notification_note_id',
'placeholder' => esc_attr__( 'Search for an inbox notification&hellip;', 'woocommerce' ),
),
'callback' => function () {
check_ajax_referer( 'debug_action', '_wpnonce' );
if ( ! isset( $_GET['delete_inbox_notification_note_id'] ) ) {
return __( 'No inbox notification selected', 'woocommerce' );
}
$note_id = wc_clean( sanitize_text_field( wp_unslash( $_GET['delete_inbox_notification_note_id'] ) ) );
$note = Notes::get_note( $note_id );
if ( ! $note ) {
return __( 'Inbox notification not found', 'woocommerce' );
}
$note->delete( true );
return __( 'Inbox notification has been deleted', 'woocommerce' );
},
);
return $tools;
}
/**
* Add ajax action for remote inbox notification search.
*
* @return void
*/
private static function ajax_action_inbox_notification_search() {
global $wpdb;
check_ajax_referer( 'search-products', 'security' );
if ( ! isset( $_GET['term'] ) ) {
wp_send_json( array() );
}
$search = wc_clean( sanitize_text_field( wp_unslash( $_GET['term'] ) ) );
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT note_id, name FROM {$wpdb->prefix}wc_admin_notes WHERE name LIKE %s",
'%' . $wpdb->esc_like( $search ) . '%'
)
);
$rows = array();
foreach ( $results as $result ) {
$rows[ $result->note_id ] = $result->name;
}
wp_send_json( $rows );
}
}

View File

@@ -25,23 +25,23 @@ class EvaluationLogger {
/**
* Logger class to use.
*
* @var WC_Logger_Interface|null
* @var \WC_Logger_Interface|null
*/
private $logger;
/**
* Logger source.
*
* @var string logger source.
* @var string Logger source.
*/
private $source = '';
/**
* EvaluationLogger constructor.
*
* @param string $slug Slug of a spec that is being evaluated.
* @param null $source Logger source.
* @param \WC_Logger_Interface $logger Logger class to use.
* @param string $slug Slug/ID of a spec that is being evaluated.
* @param string|null $source Logger source.
* @param \WC_Logger_Interface|null $logger Logger class to use. Default to using the WC logger.
*/
public function __construct( $slug, $source = null, \WC_Logger_Interface $logger = null ) {
$this->slug = $slug;
@@ -59,16 +59,13 @@ class EvaluationLogger {
/**
* Add evaluation result of a rule.
*
* @param string $rule_type name of the rule being tested.
* @param boolean $result result of a given rule.
* @param string $rule_type Name of the rule being tested.
* @param boolean $result Result of a given rule.
*/
public function add_result( $rule_type, $result ) {
array_push(
$this->results,
array(
'rule' => $rule_type,
'result' => $result ? 'passed' : 'failed',
)
$this->results[] = array(
'rule' => $rule_type,
'result' => $result ? 'passed' : 'failed',
);
}
@@ -76,7 +73,16 @@ class EvaluationLogger {
* Log the results.
*/
public function log() {
if ( false === defined( 'WC_ADMIN_DEBUG_RULE_EVALUATOR' ) || true !== constant( 'WC_ADMIN_DEBUG_RULE_EVALUATOR' ) ) {
$should_log = defined( 'WC_ADMIN_DEBUG_RULE_EVALUATOR' ) && true === constant( 'WC_ADMIN_DEBUG_RULE_EVALUATOR' );
/**
* Filter to determine if the rule evaluator should log the results.
*
* @since 9.2.0
*
* @param bool $should_log Whether the rule evaluator should log the results.
*/
if ( ! apply_filters( 'woocommerce_admin_remote_specs_evaluator_should_log', $should_log ) ) {
return;
}

View File

@@ -36,9 +36,9 @@ class RuleEvaluator {
* Evaluate the given rules as an AND operation - return false early if a
* rule evaluates to false.
*
* @param array|object $rules The rule or rules being processed.
* @param array|object $rules The rule or rules being processed.
* @param object|null $stored_state Stored state.
* @param array $logger_args Arguments for the event logger. `slug` is required.
* @param array $logger_args Arguments for the rule evaluator logger. `slug` is required.
*
* @throws \InvalidArgumentException Thrown when $logger_args is missing slug.
*
@@ -65,13 +65,16 @@ class RuleEvaluator {
throw new \InvalidArgumentException( 'Missing required field: slug in $logger_args.' );
}
array_key_exists( 'source', $logger_args ) ? $source = $logger_args['source'] : $source = null;
$source = isset( $logger_args['source'] ) ? $logger_args['source'] : null;
$evaluation_logger = new EvaluationLogger( $logger_args['slug'], $source );
}
foreach ( $rules as $rule ) {
if ( ! is_object( $rule ) ) {
$evaluation_logger && $evaluation_logger->add_result( 'rule not an object', false );
$evaluation_logger && $evaluation_logger->log();
return false;
}

View File

@@ -155,7 +155,6 @@ class WCAdminHelper {
'shop' => wc_get_page_id( 'shop' ),
'cart' => wc_get_page_id( 'cart' ),
'checkout' => wc_get_page_id( 'checkout' ),
'privacy' => wc_privacy_policy_page_id(),
'terms' => wc_terms_and_conditions_page_id(),
'coming_soon' => wc_get_page_id( 'coming_soon' ),
);

View File

@@ -47,8 +47,9 @@ class Autoloader {
*/
protected static function missing_autoloader() {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
// This message is not translated as at this point it's too early to load translations.
error_log( // phpcs:ignore
esc_html__( 'Your installation of WooCommerce is incomplete. If you installed WooCommerce from GitHub, please refer to this document to set up your development environment: https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment', 'woocommerce' )
esc_html( 'Your installation of WooCommerce is incomplete. If you installed WooCommerce from GitHub, please refer to this document to set up your development environment: https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment' )
);
}
add_action(

View File

@@ -0,0 +1,673 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Blocks\AIContent;
/**
* Patterns Dictionary class.
*/
class PatternsDictionary {
/**
* Returns the patterns' dictionary.
*
* @return array[]
*/
public static function get() {
return [
[
'name' => 'Banner',
'slug' => 'woocommerce-blocks/banner',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Up to 60% off', 'woocommerce' ),
'ai_prompt' => __( 'A four words title advertising the sale', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Holiday Sale', 'woocommerce' ),
'ai_prompt' => __( 'A two words label with the sale name', 'woocommerce' ),
],
[
'default' => __( 'Get your favorite vinyl at record-breaking prices.', 'woocommerce' ),
'ai_prompt' => __( 'The main description of the sale with at least 65 characters', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Shop vinyl records', 'woocommerce' ),
'ai_prompt' => __( 'A 3 words button text to go to the sale page', 'woocommerce' ),
],
],
],
],
[
'name' => 'Discount Banner',
'slug' => 'woocommerce-blocks/discount-banner',
'content' => [
'descriptions' => [
[
'default' => __( 'Select products', 'woocommerce' ),
'ai_prompt' => __( 'A two words description of the products on sale', 'woocommerce' ),
],
],
],
],
[
'name' => 'Discount Banner with Image',
'slug' => 'woocommerce-blocks/discount-banner-with-image',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'descriptions' => [
[
'default' => __( 'Select products', 'woocommerce' ),
'ai_prompt' => __( 'A two words description of the products on sale', 'woocommerce' ),
],
],
],
],
[
'name' => 'Featured Category Focus',
'slug' => 'woocommerce-blocks/featured-category-focus',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Black and white high-quality prints', 'woocommerce' ),
'ai_prompt' => __( 'The four words title of the featured category related to the following image description: [image.0]', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Shop prints', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the featured category', 'woocommerce' ),
],
],
],
],
[
'name' => 'Featured Category Triple',
'slug' => 'woocommerce-blocks/featured-category-triple',
'images_total' => 3,
'images_format' => 'portrait',
'content' => [
'titles' => [
[
'default' => __( 'Home decor', 'woocommerce' ),
'ai_prompt' => __( 'A one-word graphic title that encapsulates the essence of the business, inspired by the following image description: [image.0] and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image', 'woocommerce' ),
],
[
'default' => __( 'Retro photography', 'woocommerce' ),
'ai_prompt' => __( 'A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: [image.1] and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image', 'woocommerce' ),
],
[
'default' => __( 'Handmade gifts', 'woocommerce' ),
'ai_prompt' => __( 'A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: [image.2] and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image', 'woocommerce' ),
],
],
],
],
[
'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', 'woocommerce' ),
'ai_prompt' => __( 'The title of the featured products with at least 20 characters', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Sweet Organic Lemons', 'woocommerce' ),
'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.0]', 'woocommerce' ),
],
[
'default' => __( 'Fresh Organic Tomatoes', 'woocommerce' ),
'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.1]', 'woocommerce' ),
],
[
'default' => __( 'Fresh Lettuce (Washed)', 'woocommerce' ),
'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.2]', 'woocommerce' ),
],
[
'default' => __( 'Russet Organic Potatoes', 'woocommerce' ),
'ai_prompt' => __( 'The three words description of the featured product related to the following image description: [image.3]', 'woocommerce' ),
],
],
],
],
[
'name' => 'Hero Product 3 Split',
'slug' => 'woocommerce-blocks/hero-product-3-split',
'images_total' => 1,
'images_format' => 'portrait',
'content' => [
'titles' => [
[
'default' => __( 'Timeless elegance', 'woocommerce' ),
'ai_prompt' => __( 'Write a two words title for advertising the store', 'woocommerce' ),
],
[
'default' => __( 'Durable glass', 'woocommerce' ),
'ai_prompt' => __( 'Write a two words title for advertising the store', 'woocommerce' ),
],
[
'default' => __( 'Versatile charm', 'woocommerce' ),
'ai_prompt' => __( 'Write a two words title for advertising the store', 'woocommerce' ),
],
[
'default' => __( 'New: Retro Glass Jug', 'woocommerce' ),
'ai_prompt' => __( 'Write a title with less than 20 characters for advertising the store', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Elevate your table with a 330ml Retro Glass Jug, blending classic design and durable hardened glass.', 'woocommerce' ),
'ai_prompt' => __( 'Write a text with approximately 130 characters, to describe a product the business is selling', 'woocommerce' ),
],
[
'default' => __( 'Crafted from resilient thick glass, this jug ensures lasting quality, making it perfect for everyday use with a touch of vintage charm.', 'woocommerce' ),
'ai_prompt' => __( 'Write a text with approximately 130 characters, to describe a product the business is selling', 'woocommerce' ),
],
[
'default' => __( "The Retro Glass Jug's classic silhouette effortlessly complements any setting, making it the ideal choice for serving beverages with style and flair.", 'woocommerce' ),
'ai_prompt' => __( 'Write a long text, with at least 130 characters, to describe a product the business is selling', 'woocommerce' ),
],
],
],
],
[
'name' => 'Hero Product Chessboard',
'slug' => 'woocommerce-blocks/hero-product-chessboard',
'images_total' => 2,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Quality Materials', 'woocommerce' ),
'ai_prompt' => __( 'A two words title describing the first displayed product feature', 'woocommerce' ),
],
[
'default' => __( 'Unique design', 'woocommerce' ),
'ai_prompt' => __( 'A two words title describing the second displayed product feature', 'woocommerce' ),
],
[
'default' => __( 'Make your house feel like home', 'woocommerce' ),
'ai_prompt' => __( 'A two words title describing the fourth displayed product feature', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'We use only the highest-quality materials in our products, ensuring that they look great and last for years to come.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the product feature with at least 115 characters', 'woocommerce' ),
],
[
'default' => __( 'From bold prints to intricate details, our products are a perfect combination of style and function.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the product feature with at least 115 characters', 'woocommerce' ),
],
[
'default' => __( 'Add a touch of charm and coziness this holiday season with a wide selection of hand-picked decorations — from minimalist vases to designer furniture.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the product feature with at least 115 characters', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Shop home decor', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the product page', 'woocommerce' ),
],
],
],
],
[
'name' => 'Hero Product Split',
'slug' => 'woocommerce-blocks/hero-product-split',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Keep dry with 50% off rain jackets', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the product the store is selling with at least 35 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Just Arrived Full Hero',
'slug' => 'woocommerce-blocks/just-arrived-full-hero',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Sound like no other', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 10 characters', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Experience your music like never before with our latest generation of hi-fidelity headphones.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the product collection with at least 35 characters', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Shop now', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the product collection page', 'woocommerce' ),
],
],
],
],
[
'name' => 'Product Collection Banner',
'slug' => 'woocommerce-blocks/product-collection-banner',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Brand New for the Holidays', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 25 characters related to the following image description: [image.0]', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Check out our brand new collection of holiday products and find the right gift for anyone.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the product collection with at least 90 characters', 'woocommerce' ),
],
],
],
],
[
'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 with at least 30 characters', 'woocommerce' ),
],
],
],
],
[
'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', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the product collection with at least 20 characters related to the following image descriptions: [image.0], [image.1]', 'woocommerce' ),
],
[
'default' => __( 'For the gamers', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the product collection with at least 15 characters related to the following image descriptions: [image.2], [image.3]', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Shop tech', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the product collection page', 'woocommerce' ),
],
[
'default' => __( 'Shop games', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the product collection page', 'woocommerce' ),
],
],
],
],
[
'name' => 'Product Collections Newest Arrivals',
'slug' => 'woocommerce-blocks/product-collections-newest-arrivals',
'content' => [
'titles' => [
[
'default' => __( 'Our newest arrivals', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 20 characters', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'More new products', 'woocommerce' ),
'ai_prompt' => __( 'The button text to go to the product collection page with at least 15 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Product Collection 4 Columns',
'slug' => 'woocommerce-blocks/product-collection-4-columns',
'content' => [
'titles' => [
[
'default' => __( 'Staff picks', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the displayed product collection with at least 20 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Product Collection 5 Columns',
'slug' => 'woocommerce-blocks/product-collection-5-columns',
'content' => [
'titles' => [
[
'default' => __( 'Our latest and greatest', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase with that advertises the product collection with at least 20 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Product Gallery',
'slug' => 'woocommerce-blocks/product-query-product-gallery',
'content' => [
'titles' => [
[
'default' => __( 'Bestsellers', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the featured products with at least 10 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Featured Products 2 Columns',
'slug' => 'woocommerce-blocks/featured-products-2-cols',
'content' => [
'titles' => [
[
'default' => __( 'Fan favorites', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the featured products with at least 10 characters', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Get ready to start the season right. All the fan favorites in one place at the best price.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the featured products with at least 90 characters', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Shop All', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the featured products page', 'woocommerce' ),
],
],
],
],
[
'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', 'woocommerce' ),
'ai_prompt' => __( 'A three words title that advertises a product related to the following image description: [image.0]', 'woocommerce' ),
],
[
'default' => __( '100% Woolen', 'woocommerce' ),
'ai_prompt' => __( 'A two words title that advertises a product feature', 'woocommerce' ),
],
[
'default' => __( 'Fits your wardrobe', 'woocommerce' ),
'ai_prompt' => __( 'A three words title that advertises a product feature', 'woocommerce' ),
],
[
'default' => __( 'Versatile', 'woocommerce' ),
'ai_prompt' => __( 'An one word title that advertises a product feature', 'woocommerce' ),
],
[
'default' => __( 'Normal Fit', 'woocommerce' ),
'ai_prompt' => __( 'A two words title that advertises a product feature', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Perfect for any look featuring a mid-rise, relax fitting silhouette.', 'woocommerce' ),
'ai_prompt' => __( 'The description of a product with at least 65 characters related to the following image: [image.0]', 'woocommerce' ),
],
[
'default' => __( 'Reflect your fashionable style.', 'woocommerce' ),
'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ),
],
[
'default' => __( 'Half tuck into your pants or layer over.', 'woocommerce' ),
'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ),
],
[
'default' => __( 'Button-down front for any type of mood or look.', 'woocommerce' ),
'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ),
],
[
'default' => __( '42% Cupro 34% Linen 24% Viscose', 'woocommerce' ),
'ai_prompt' => __( 'The description of a product feature with at least 30 characters', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'View product', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the product page', 'woocommerce' ),
],
],
],
],
[
'name' => 'Shop by Price',
'slug' => 'woocommerce-blocks/shop-by-price',
'content' => [
'titles' => [
[
'default' => __( 'Outdoor Furniture & Accessories', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the first product collection with at least 30 characters', 'woocommerce' ),
],
[
'default' => __( 'Summer Dinning', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the second product collection with at least 20 characters', 'woocommerce' ),
],
[
'default' => "Women's Styles",
'ai_prompt' => __( 'An impact phrase that advertises the third product collection with at least 20 characters', 'woocommerce' ),
],
[
'default' => "Kids' Styles",
'ai_prompt' => __( 'An impact phrase that advertises the fourth product collection with at least 20 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Small Discount Banner with Image',
'slug' => 'woocommerce-blocks/small-discount-banner-with-image',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Chairs', 'woocommerce' ),
'ai_prompt' => __( 'A single word that advertises the product and is related to the following image description: [image.0]', 'woocommerce' ),
],
],
],
],
[
'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' => __( 'Stay in the loop', 'woocommerce' ),
'ai_prompt' => __( 'A phrase that advertises the social media accounts of the store with at least 25 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Alternating Image and Text',
'slug' => 'woocommerce-blocks/alt-image-and-text',
'images_total' => 2,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Our products', 'woocommerce' ),
'ai_prompt' => __( 'A two words impact phrase that advertises the products', 'woocommerce' ),
],
[
'default' => __( 'Sustainable blends, stylish accessories', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the products with at least 40 characters and related to the following image description: [image.0]', 'woocommerce' ),
],
[
'default' => __( 'About us', 'woocommerce' ),
'ai_prompt' => __( 'A two words impact phrase that advertises the brand', 'woocommerce' ),
],
[
'default' => __( 'Committed to a greener lifestyle', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the brand with at least 50 characters related to the following image description: [image.1]', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Indulge in the finest organic coffee beans, teas, and hand-picked accessories, all locally sourced and sustainable for a mindful lifestyle.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the products with at least 180 characters', 'woocommerce' ),
],
[
'default' => "Our passion is crafting mindful moments with locally sourced, organic, and sustainable products. We're more than a store; we're your path to a community-driven, eco-friendly lifestyle that embraces premium quality.",
'ai_prompt' => __( 'A description of the products with at least 180 characters', 'woocommerce' ),
],
[
'default' => __( 'Locally sourced ingredients', 'woocommerce' ),
'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ),
],
[
'default' => __( 'Premium organic blends', 'woocommerce' ),
'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ),
],
[
'default' => __( 'Hand-picked accessories', 'woocommerce' ),
'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ),
],
[
'default' => __( 'Sustainable business practices', 'woocommerce' ),
'ai_prompt' => __( 'A three word description of the products', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Meet us', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the product page', 'woocommerce' ),
],
],
],
],
[
'name' => 'Testimonials 3 Columns',
'slug' => 'woocommerce-blocks/testimonials-3-columns',
'content' => [
'titles' => [
[
'default' => __( 'Eclectic finds, ethical delights', 'woocommerce' ),
'ai_prompt' => __( 'Write a short title advertising a testimonial from a customer', 'woocommerce' ),
],
[
'default' => __( 'Sip, Shop, Savor', 'woocommerce' ),
'ai_prompt' => __( 'Write a short title advertising a testimonial from a customer', 'woocommerce' ),
],
[
'default' => __( 'LOCAL LOVE', 'woocommerce' ),
'ai_prompt' => __( 'Write a short title advertising a testimonial from a customer', 'woocommerce' ),
],
[
'default' => __( 'What our customers say', 'woocommerce' ),
'ai_prompt' => __( 'Write just 4 words to advertise testimonials from customers', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Transformed my daily routine with unique, eco-friendly treasures. Exceptional quality and service. Proud to support a store that aligns with my values.', 'woocommerce' ),
'ai_prompt' => __( 'Write the testimonial from a customer with approximately 150 characters', 'woocommerce' ),
],
[
'default' => __( 'The organic coffee beans are a revelation. Each sip feels like a journey. Beautifully crafted accessories add a touch of elegance to my home.', 'woocommerce' ),
'ai_prompt' => __( 'Write the testimonial from a customer with approximately 150 characters', 'woocommerce' ),
],
[
'default' => __( 'From sustainably sourced teas to chic vases, this store is a treasure trove. Love knowing my purchases contribute to a greener planet.', 'woocommerce' ),
'ai_prompt' => __( 'Write the testimonial from a customer with approximately 150 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Testimonials Single',
'slug' => 'woocommerce-blocks/testimonials-single',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'A brewtiful experience :-)', 'woocommerce' ),
'ai_prompt' => __( 'A two words title that advertises the testimonial', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'Exceptional flavors, sustainable choices. The carefully curated collection of coffee pots and accessories turned my kitchen into a haven of style and taste.', 'woocommerce' ),
'ai_prompt' => __( 'A description of the testimonial with at least 225 characters', 'woocommerce' ),
],
],
],
],
[
'name' => 'Featured Category Cover Image',
'slug' => 'woocommerce-blocks/featured-category-cover-image',
'images_total' => 1,
'images_format' => 'landscape',
'content' => [
'titles' => [
[
'default' => __( 'Sit back and relax', 'woocommerce' ),
'ai_prompt' => __( 'A description for a product with at least 20 characters', 'woocommerce' ),
],
],
'descriptions' => [
[
'default' => __( 'With a wide range of designer chairs to elevate your living space.', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the products with at least 55 characters', 'woocommerce' ),
],
],
'buttons' => [
[
'default' => __( 'Shop chairs', 'woocommerce' ),
'ai_prompt' => __( 'A two words button text to go to the shop page', 'woocommerce' ),
],
],
],
],
[
'name' => 'Product Collection: Featured Products 5 Columns',
'slug' => 'woocommerce-blocks/product-collection-featured-products-5-columns',
'content' => [
'titles' => [
[
'default' => __( 'Shop new arrivals', 'woocommerce' ),
'ai_prompt' => __( 'An impact phrase that advertises the newest additions to the store with at least 20 characters', 'woocommerce' ),
],
],
],
],
];
}
}

View File

@@ -63,7 +63,7 @@ class PatternsHelper {
/**
* Upsert the patterns AI data.
*
* @param array $patterns_dictionary The patterns dictionary.
* @param array $patterns_dictionary The patterns' dictionary.
*
* @return WP_Error|null
*/
@@ -92,18 +92,12 @@ class PatternsHelper {
* @return array|WP_Error Returns pattern dictionary or WP_Error on failure.
*/
public static function get_patterns_dictionary( $pattern_slug = null ) {
$patterns_dictionary_file = plugin_dir_path( __FILE__ ) . 'dictionary.json';
$default_patterns_dictionary = PatternsDictionary::get();
if ( ! file_exists( $patterns_dictionary_file ) ) {
if ( empty( $default_patterns_dictionary ) ) {
return new WP_Error( 'missing_patterns_dictionary', __( 'The patterns dictionary is missing.', 'woocommerce' ) );
}
$default_patterns_dictionary = wp_json_file_decode( $patterns_dictionary_file, array( 'associative' => true ) );
if ( json_last_error() !== JSON_ERROR_NONE ) {
return new WP_Error( 'json_decode_error', __( 'Error decoding JSON.', 'woocommerce' ) );
}
$patterns_dictionary = '';
$ai_connection_allowed = get_option( 'woocommerce_blocks_allow_ai_connection' );

View File

@@ -402,13 +402,13 @@ class UpdatePatterns {
* @return mixed|WP_Error|null
*/
public static function get_patterns_dictionary() {
$patterns_dictionary = plugin_dir_path( __FILE__ ) . 'dictionary.json';
$patterns_dictionary = PatternsDictionary::get();
if ( ! file_exists( $patterns_dictionary ) ) {
if ( empty( $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 ) );
return $patterns_dictionary;
}
/**

View File

@@ -26,7 +26,7 @@ class UpdateProducts {
'price' => 249,
],
[
'title' => 'Black and White Summer Portrait',
'title' => 'Black and White',
'image' => 'assets/images/pattern-placeholders/white-black-black-and-white-photograph-monochrome-photography.jpg',
'description' => 'This 24" x 30" high-quality print just exudes summer. Hang it on the wall and forget about the world outside.',
'price' => 115,
@@ -113,7 +113,7 @@ class UpdateProducts {
$products_to_create = max( 0, 6 - $real_products_count - $dummy_products_count );
while ( $products_to_create > 0 ) {
$this->create_new_product( self::DUMMY_PRODUCTS[ $products_to_create - 1 ] );
$products_to_create--;
--$products_to_create;
}
// Identify dummy products that need to have their content updated.
@@ -327,7 +327,7 @@ class UpdateProducts {
public function assign_ai_selected_images_to_dummy_products( $dummy_products_to_update, $ai_selected_images ) {
$products_information_list = [];
$dummy_products_count = count( $dummy_products_to_update );
for ( $i = 0; $i < $dummy_products_count; $i ++ ) {
for ( $i = 0; $i < $dummy_products_count; $i++ ) {
$image_src = $ai_selected_images[ $i ]['URL'] ?? '';
if ( wc_is_valid_url( $image_src ) ) {
@@ -396,7 +396,7 @@ class UpdateProducts {
$ai_request_retries = 0;
$success = false;
while ( $ai_request_retries < 5 && ! $success ) {
$ai_request_retries ++;
++$ai_request_retries;
$ai_response = $ai_connection->fetch_ai_response( $token, $formatted_prompt, 30 );
if ( is_wp_error( $ai_response ) ) {
continue;
@@ -464,7 +464,7 @@ class UpdateProducts {
$this->product_update( $product, $product_image_id, self::DUMMY_PRODUCTS[ $i ]['title'], self::DUMMY_PRODUCTS[ $i ]['description'], self::DUMMY_PRODUCTS[ $i ]['price'] );
$i++;
++$i;
}
}

View File

@@ -1,656 +0,0 @@
[
{
"name": "Banner",
"slug": "woocommerce-blocks/banner",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Up to 60% off",
"ai_prompt": "A four words title advertising the sale"
}
],
"descriptions": [
{
"default": "Holiday Sale",
"ai_prompt": "A two words label with the sale name"
},
{
"default": "Get your favorite vinyl at record-breaking prices.",
"ai_prompt": "The main description of the sale with at least 65 characters"
}
],
"buttons": [
{
"default": "Shop vinyl records",
"ai_prompt": "A 3 words button text to go to the sale page"
}
]
}
},
{
"name": "Discount Banner",
"slug": "woocommerce-blocks/discount-banner",
"content": {
"descriptions": [
{
"default": "Select products",
"ai_prompt": "A two words 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 two words 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": "Black and white high-quality prints",
"ai_prompt": "The four words title of the featured category related to the following image description: {image.0}"
}
],
"buttons": [
{
"default": "Shop prints",
"ai_prompt": "A two words button text to go to the featured category"
}
]
}
},
{
"name": "Featured Category Triple",
"slug": "woocommerce-blocks/featured-category-triple",
"images_total": 3,
"images_format": "portrait",
"content": {
"titles": [
{
"default": "Home decor",
"ai_prompt": "A one-word graphic title that encapsulates the essence of the business, inspired by the following image description: {image.0} and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image"
},
{
"default": "Retro photography",
"ai_prompt": "A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: {image.1} and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image"
},
{
"default": "Handmade gifts",
"ai_prompt": "A two-words graphic title that encapsulates the essence of the business, inspired by the following image description: {image.2} and the nature of the business. The title should reflect the key elements and characteristics of the business, as portrayed in the image"
}
]
}
},
{
"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 with at least 20 characters"
}
],
"descriptions": [
{
"default": "Sweet Organic Lemons",
"ai_prompt": "The three words description of the featured product related to the following image description: {image.0}"
},
{
"default": "Fresh Organic Tomatoes",
"ai_prompt": "The three words description of the featured product related to the following image description: {image.1}"
},
{
"default": "Fresh Lettuce (Washed)",
"ai_prompt": "The three words description of the featured product related to the following image description: {image.2}"
},
{
"default": "Russet Organic Potatoes",
"ai_prompt": "The three words description of the featured product related to the following image description: {image.3}"
}
]
}
},
{
"name": "Hero Product 3 Split",
"slug": "woocommerce-blocks/hero-product-3-split",
"images_total": 1,
"images_format": "portrait",
"content": {
"titles": [
{
"default": "Timeless elegance",
"ai_prompt": "Write a two words title for advertising the store"
},
{
"default": "Durable glass",
"ai_prompt": "Write a two words title for advertising the store"
},
{
"default": "Versatile charm",
"ai_prompt": "Write a two words title for advertising the store"
},
{
"default": "New: Retro Glass Jug",
"ai_prompt": "Write a title with less than 20 characters for advertising the store"
}
],
"descriptions": [
{
"default": "Elevate your table with a 330ml Retro Glass Jug, blending classic design and durable hardened glass.",
"ai_prompt": "Write a text with approximately 130 characters, to describe a product the business is selling"
},
{
"default": "Crafted from resilient thick glass, this jug ensures lasting quality, making it perfect for everyday use with a touch of vintage charm.",
"ai_prompt": "Write a text with approximately 130 characters, to describe a product the business is selling"
},
{
"default": "The Retro Glass Jug's classic silhouette effortlessly complements any setting, making it the ideal choice for serving beverages with style and flair.",
"ai_prompt": "Write a long text, with at least 130 characters, to describe a product the business is selling"
}
]
}
},
{
"name": "Hero Product Chessboard",
"slug": "woocommerce-blocks/hero-product-chessboard",
"images_total": 2,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Quality Materials",
"ai_prompt": "A two words title describing the first displayed product feature"
},
{
"default": "Unique design",
"ai_prompt": "A two words title describing the second displayed product feature"
},
{
"default": "Make your house feel like home",
"ai_prompt": "A two words title describing the fourth displayed product feature"
}
],
"descriptions": [
{
"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 product feature with at least 115 characters"
},
{
"default": "From bold prints to intricate details, our products are a perfect combination of style and function.",
"ai_prompt": "A description of the product feature with at least 115 characters"
},
{
"default": "Add a touch of charm and coziness this holiday season with a wide selection of hand-picked decorations — from minimalist vases to designer furniture.",
"ai_prompt": "A description of the product feature with at least 115 characters"
}
],
"buttons": [
{
"default": "Shop home decor",
"ai_prompt": "A two words button text to go to the product page"
}
]
}
},
{
"name": "Hero Product Split",
"slug": "woocommerce-blocks/hero-product-split",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Keep dry with 50% off rain jackets",
"ai_prompt": "An impact phrase that advertises the product the store is selling with at least 35 characters"
}
]
}
},
{
"name": "Just Arrived Full Hero",
"slug": "woocommerce-blocks/just-arrived-full-hero",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Sound like no other",
"ai_prompt": "An impact phrase that advertises the displayed product collection with at least 10 characters"
}
],
"descriptions": [
{
"default": "Experience your music like never before with our latest generation of hi-fidelity headphones.",
"ai_prompt": "A description of the product collection with at least 35 characters"
}
],
"buttons": [
{
"default": "Shop now",
"ai_prompt": "A two words 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 with at least 25 characters related to the following image description: {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 with at least 90 characters"
}
]
}
},
{
"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 with at least 30 characters"
}
]
}
},
{
"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 product collection with at least 20 characters related to the following image descriptions: {image.0}, {image.1}"
},
{
"default": "For the gamers",
"ai_prompt": "An impact phrase that advertises the product collection with at least 15 characters related to the following image descriptions: {image.2}, {image.3}"
}
],
"buttons": [
{
"default": "Shop tech",
"ai_prompt": "A two words button text to go to the product collection page"
},
{
"default": "Shop games",
"ai_prompt": "A two words button text to go to the 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 with at least 20 characters"
}
],
"buttons": [
{
"default": "More new products",
"ai_prompt": "The button text to go to the product collection page with at least 15 characters"
}
]
}
},
{
"name": "Product Collection 4 Columns",
"slug": "woocommerce-blocks/product-collection-4-columns",
"content": {
"titles": [
{
"default": "Staff picks",
"ai_prompt": "An impact phrase that advertises the displayed product collection with at least 20 characters"
}
]
}
},
{
"name": "Product Collection 5 Columns",
"slug": "woocommerce-blocks/product-collection-5-columns",
"content": {
"titles": [
{
"default": "Our latest and greatest",
"ai_prompt": "An impact phrase with that advertises the product collection with at least 20 characters"
}
]
}
},
{
"name": "Product Gallery",
"slug": "woocommerce-blocks/product-query-product-gallery",
"content": {
"titles": [
{
"default": "Bestsellers",
"ai_prompt": "An impact phrase that advertises the featured products with at least 10 characters"
}
]
}
},
{
"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 featured products with at least 10 characters"
}
],
"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 with at least 90 characters"
}
],
"buttons": [
{
"default": "Shop All",
"ai_prompt": "A two words 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": "A three words title that advertises a product related to the following image description: {image.0}"
},
{
"default": "100% Woolen",
"ai_prompt": "A two words title that advertises a product feature"
},
{
"default": "Fits your wardrobe",
"ai_prompt": "A three words title that advertises a product feature"
},
{
"default": "Versatile",
"ai_prompt": "An one word title that advertises a product feature"
},
{
"default": "Normal Fit",
"ai_prompt": "A two words title that advertises a product feature"
}
],
"descriptions": [
{
"default": "Perfect for any look featuring a mid-rise, relax fitting silhouette.",
"ai_prompt": "The description of a product with at least 65 characters related to the following image: {image.0}"
},
{
"default": "Reflect your fashionable style.",
"ai_prompt": "The description of a product feature with at least 30 characters"
},
{
"default": "Half tuck into your pants or layer over.",
"ai_prompt": "The description of a product feature with at least 30 characters"
},
{
"default": "Button-down front for any type of mood or look.",
"ai_prompt": "The description of a product feature with at least 30 characters"
},
{
"default": "42% Cupro 34% Linen 24% Viscose",
"ai_prompt": "The description of a product feature with at least 30 characters"
}
],
"buttons": [
{
"default": "View product",
"ai_prompt": "A two words 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 with at least 30 characters"
},
{
"default": "Summer Dinning",
"ai_prompt": "An impact phrase that advertises the second product collection with at least 20 characters"
},
{
"default": "Women's Styles",
"ai_prompt": "An impact phrase that advertises the third product collection with at least 20 characters"
},
{
"default": "Kids' Styles",
"ai_prompt": "An impact phrase that advertises the fourth product collection with at least 20 characters"
}
]
}
},
{
"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": "A single word that advertises the product and is related to the following image description: {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": "Stay in the loop",
"ai_prompt": "A phrase that advertises the social media accounts of the store with at least 25 characters"
}
]
}
},
{
"name": "Alternating Image and Text",
"slug": "woocommerce-blocks/alt-image-and-text",
"images_total": 2,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Our products",
"ai_prompt": "A two words impact phrase that advertises the products"
},
{
"default": "Sustainable blends, stylish accessories",
"ai_prompt": "An impact phrase that advertises the products with at least 40 characters and related to the following image description: {image.0}"
},
{
"default": "About us",
"ai_prompt": "A two words impact phrase that advertises the brand"
},
{
"default": "Committed to a greener lifestyle",
"ai_prompt": "An impact phrase that advertises the brand with at least 50 characters related to the following image description: {image.1}"
}
],
"descriptions": [
{
"default": "Indulge in the finest organic coffee beans, teas, and hand-picked accessories, all locally sourced and sustainable for a mindful lifestyle.",
"ai_prompt": "A description of the products with at least 180 characters"
},
{
"default": "Our passion is crafting mindful moments with locally sourced, organic, and sustainable products. We're more than a store; we're your path to a community-driven, eco-friendly lifestyle that embraces premium quality.",
"ai_prompt": "A description of the products with at least 180 characters"
},
{
"default": "Locally sourced ingredients",
"ai_prompt": "A three word description of the products"
},
{
"default": "Premium organic blends",
"ai_prompt": "A three word description of the products"
},
{
"default": "Hand-picked accessories",
"ai_prompt": "A three word description of the products"
},
{
"default": "Sustainable business practices",
"ai_prompt": "A three word description of the products"
}
],
"buttons": [
{
"default": "Meet us",
"ai_prompt": "A two words button text to go to the product page"
}
]
}
},
{
"name": "Testimonials 3 Columns",
"slug": "woocommerce-blocks/testimonials-3-columns",
"content": {
"titles": [
{
"default": "Eclectic finds, ethical delights",
"ai_prompt": "Write a short title advertising a testimonial from a customer"
},
{
"default": "Sip, Shop, Savor",
"ai_prompt": "Write a short title advertising a testimonial from a customer"
},
{
"default": "LOCAL LOVE",
"ai_prompt": "Write a short title advertising a testimonial from a customer"
},
{
"default": "What our customers say",
"ai_prompt": "Write just 4 words to advertise testimonials from customers"
}
],
"descriptions": [
{
"default": "Transformed my daily routine with unique, eco-friendly treasures. Exceptional quality and service. Proud to support a store that aligns with my values.",
"ai_prompt": "Write the testimonial from a customer with approximately 150 characters"
},
{
"default": "The organic coffee beans are a revelation. Each sip feels like a journey. Beautifully crafted accessories add a touch of elegance to my home.",
"ai_prompt": "Write the testimonial from a customer with approximately 150 characters"
},
{
"default": "From sustainably sourced teas to chic vases, this store is a treasure trove. Love knowing my purchases contribute to a greener planet.",
"ai_prompt": "Write the testimonial from a customer with approximately 150 characters"
}
]
}
},
{
"name": "Testimonials Single",
"slug": "woocommerce-blocks/testimonials-single",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "A brewtiful experience :-)",
"ai_prompt": "A two words title that advertises the testimonial"
}
],
"descriptions": [
{
"default": "Exceptional flavors, sustainable choices. The carefully curated collection of coffee pots and accessories turned my kitchen into a haven of style and taste.",
"ai_prompt": "A description of the testimonial with at least 225 characters"
}
]
}
},
{
"name": "Featured Category Cover Image",
"slug": "woocommerce-blocks/featured-category-cover-image",
"images_total": 1,
"images_format": "landscape",
"content": {
"titles": [
{
"default": "Sit back and relax",
"ai_prompt": "A description for a product with at least 20 characters"
}
],
"descriptions": [
{
"default": "With a wide range of designer chairs to elevate your living space.",
"ai_prompt": "An impact phrase that advertises the products with at least 55 characters"
}
],
"buttons": [
{
"default": "Shop chairs",
"ai_prompt": "A two words 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 with at least 20 characters"
}
]
}
}
]

View File

@@ -40,6 +40,7 @@ final class AssetsController {
add_action( 'admin_enqueue_scripts', array( $this, 'update_block_style_dependencies' ), 20 );
add_action( 'wp_enqueue_scripts', array( $this, 'update_block_settings_dependencies' ), 100 );
add_action( 'admin_enqueue_scripts', array( $this, 'update_block_settings_dependencies' ), 100 );
add_filter( 'js_do_concat', array( $this, 'skip_boost_minification_for_cart_checkout' ), 10, 2 );
}
/**
@@ -62,9 +63,14 @@ final class AssetsController {
// The price package is shared externally so has no blocks prefix.
$this->api->register_script( 'wc-price-format', 'assets/client/blocks/price-format.js', array(), false );
$this->api->register_script( 'wc-blocks-vendors-frontend', $this->api->get_block_asset_build_path( 'wc-blocks-vendors-frontend' ), array(), false );
$this->api->register_script( 'wc-blocks-checkout', 'assets/client/blocks/blocks-checkout.js', array( 'wc-blocks-vendors-frontend' ) );
$this->api->register_script( 'wc-blocks-components', 'assets/client/blocks/blocks-components.js', array( 'wc-blocks-vendors-frontend' ) );
// Vendor scripts for blocks frontends (not including cart and checkout).
$this->api->register_script( 'wc-blocks-frontend-vendors', $this->api->get_block_asset_build_path( 'wc-blocks-frontend-vendors-frontend' ), array(), true );
// Cart and checkout frontend scripts.
$this->api->register_script( 'wc-cart-checkout-vendors', $this->api->get_block_asset_build_path( 'wc-cart-checkout-vendors-frontend' ), array(), true );
$this->api->register_script( 'wc-cart-checkout-base', $this->api->get_block_asset_build_path( 'wc-cart-checkout-base-frontend' ), array(), true );
$this->api->register_script( 'wc-blocks-checkout', 'assets/client/blocks/blocks-checkout.js' );
$this->api->register_script( 'wc-blocks-components', 'assets/client/blocks/blocks-components.js' );
// Register the interactivity components here for now.
$this->api->register_script( 'wc-interactivity-dropdown', 'assets/client/blocks/wc-interactivity-dropdown.js', array() );
@@ -253,6 +259,23 @@ final class AssetsController {
return $src;
}
/**
* Skip Jetpack Boost minification on older versions of Jetpack Boost where it causes issues.
*
* @param mixed $do_concat Whether to concatenate the script or not.
* @param mixed $handle The script handle.
* @return mixed
*/
public function skip_boost_minification_for_cart_checkout( $do_concat, $handle ) {
$boost_is_outdated = defined( 'JETPACK_BOOST_VERSION' ) && version_compare( JETPACK_BOOST_VERSION, '3.4.2', '<' );
$scripts_to_ignore = [
'wc-cart-checkout-vendors',
'wc-cart-checkout-base',
];
return $boost_is_outdated && in_array( $handle, $scripts_to_ignore, true ) ? false : $do_concat;
}
/**
* Add body classes to the frontend and within admin.
*

View File

@@ -34,6 +34,8 @@ use WP_Error;
* @internal
*/
class BlockPatterns {
const CATEGORIES_PREFIXES = [ '_woo_', '_dotcom_imported_' ];
/**
* Path to the patterns' directory.
*
@@ -145,6 +147,8 @@ class BlockPatterns {
return;
}
$patterns = $this->parse_categories( $patterns );
foreach ( $patterns as $pattern ) {
$pattern['slug'] = $pattern['name'];
$pattern['content'] = $pattern['html'];
@@ -152,4 +156,33 @@ class BlockPatterns {
$this->pattern_registry->register_block_pattern( $pattern['ID'], $pattern, $this->dictionary );
}
}
/**
* Parse prefixed categories from the PTK patterns into the actual WooCommerce categories.
*
* @param array $patterns The patterns to parse.
* @return array The parsed patterns.
*/
private function parse_categories( array $patterns ) {
return array_map(
function ( $pattern ) {
$pattern['categories'] = array_map(
function ( $category ) {
foreach ( self::CATEGORIES_PREFIXES as $prefix ) {
if ( strpos( $category['title'], $prefix ) !== false ) {
$parsed_category = str_replace( $prefix, '', $category['title'] );
$parsed_category = str_replace( '_', ' ', $parsed_category );
$category['title'] = ucfirst( $parsed_category );
}
}
return $category;
},
$pattern['categories']
);
return $pattern;
},
$patterns
);
}
}

View File

@@ -28,76 +28,9 @@ class BlockTemplatesController {
add_filter( 'get_block_template', array( $this, 'add_block_template_details' ), 10, 3 );
add_filter( 'get_block_templates', array( $this, 'add_block_templates' ), 10, 3 );
add_filter( 'taxonomy_template_hierarchy', array( $this, 'add_archive_product_to_eligible_for_fallback_templates' ), 10, 1 );
add_action( 'after_switch_theme', array( $this, 'check_should_use_blockified_product_grid_templates' ), 10, 2 );
if ( wc_current_theme_is_fse_theme() ) {
// 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.
add_filter(
'block_type_metadata_settings',
function ( $settings, $metadata ) {
if (
isset( $metadata['name'], $settings['render_callback'] ) &&
'core/template-part' === $metadata['name'] &&
in_array( $settings['render_callback'], array( 'render_block_core_template_part', 'gutenberg_render_block_core_template_part' ), true )
) {
$settings['render_callback'] = array( $this, 'render_woocommerce_template_part' );
}
return $settings;
},
10,
2
);
// Prevents shortcodes in templates having their HTML content broken by wpautop.
// @see https://core.trac.wordpress.org/ticket/58366 for more info.
add_filter(
'block_type_metadata_settings',
function ( $settings, $metadata ) {
if (
isset( $metadata['name'], $settings['render_callback'] ) &&
'core/shortcode' === $metadata['name']
) {
$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, 'wc-empty-cart-message' ) || strstr( $content, 'woocommerce-checkout-form' ) ) {
// Return early before wpautop runs again.
return $content;
}
$render_callback = $settings['original_render_callback'];
return $render_callback( $attributes, $content );
};
}
return $settings;
},
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'] ), array( 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_filter( 'block_type_metadata_settings', array( $this, 'add_plugin_templates_parts_support' ), 10, 2 );
add_filter( 'block_type_metadata_settings', array( $this, 'prevent_shortcodes_html_breakage' ), 10, 2 );
add_action( 'current_screen', array( $this, 'hide_template_selector_in_cart_checkout_pages' ), 10 );
}
/**
@@ -214,22 +147,69 @@ class BlockTemplatesController {
}
/**
* Checks the old and current themes and determines if the "wc_blocks_use_blockified_product_grid_block_as_template"
* option need to be updated accordingly.
* 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.
*
* @param array $settings Array of determined settings for registering a block type.
* @param array $metadata Metadata provided for registering a block type.
*/
public function add_plugin_templates_parts_support( $settings, $metadata ) {
if (
isset( $metadata['name'], $settings['render_callback'] ) &&
'core/template-part' === $metadata['name'] &&
in_array( $settings['render_callback'], array( 'render_block_core_template_part', 'gutenberg_render_block_core_template_part' ), true )
) {
$settings['render_callback'] = array( $this, 'render_woocommerce_template_part' );
}
return $settings;
}
/**
* Prevents shortcodes in templates having their HTML content broken by wpautop.
*
* @see https://core.trac.wordpress.org/ticket/58366 for more info.
*
* @param array $settings Array of determined settings for registering a block type.
* @param array $metadata Metadata provided for registering a block type.
*/
public function prevent_shortcodes_html_breakage( $settings, $metadata ) {
if (
isset( $metadata['name'], $settings['render_callback'] ) &&
'core/shortcode' === $metadata['name']
) {
$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, 'wc-empty-cart-message' ) || strstr( $content, 'woocommerce-checkout-form' ) ) {
// Return early before wpautop runs again.
return $content;
}
$render_callback = $settings['original_render_callback'];
return $render_callback( $attributes, $content );
};
}
return $settings;
}
/**
* 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.
*
* @param string $old_name Old theme name.
* @param \WP_Theme $old_theme Instance of the old theme.
* @return void
*/
public function check_should_use_blockified_product_grid_templates( $old_name, $old_theme ) {
if ( ! wc_current_theme_is_fse_theme() ) {
update_option( Options::WC_BLOCK_USE_BLOCKIFIED_PRODUCT_GRID_BLOCK_AS_TEMPLATE, wc_bool_to_string( false ) );
public function hide_template_selector_in_cart_checkout_pages() {
if ( ! is_admin() ) {
return;
}
if ( ! $old_theme->is_block_theme() && wc_current_theme_is_fse_theme() ) {
update_option( Options::WC_BLOCK_USE_BLOCKIFIED_PRODUCT_GRID_BLOCK_AS_TEMPLATE, wc_bool_to_string( true ) );
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'] ), array( 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; }' );
}
}
@@ -355,16 +335,26 @@ class BlockTemplatesController {
continue;
}
$is_not_custom = false === array_search(
$possible_template_ids = [
$theme_slug . '//' . $template_file->slug,
array_column( $query_result, 'id' ),
true
);
$theme_slug . '//' . BlockTemplateUtils::DIRECTORY_NAMES['TEMPLATE_PARTS'] . '/' . $template_file->slug,
$theme_slug . '//' . BlockTemplateUtils::DIRECTORY_NAMES['DEPRECATED_TEMPLATE_PARTS'] . '/' . $template_file->slug,
];
$is_custom = false;
$query_result_template_ids = array_column( $query_result, 'id' );
foreach ( $possible_template_ids as $template_id ) {
if ( in_array( $template_id, $query_result_template_ids, true ) ) {
$is_custom = true;
break;
}
}
$fits_slug_query =
! isset( $query['slug__in'] ) || in_array( $template_file->slug, $query['slug__in'], true );
$fits_area_query =
! isset( $query['area'] ) || ( property_exists( $template_file, 'area' ) && $template_file->area === $query['area'] );
$should_include = $is_not_custom && $fits_slug_query && $fits_area_query;
$should_include = ! $is_custom && $fits_slug_query && $fits_area_query;
if ( $should_include ) {
$template = BlockTemplateUtils::build_template_result_from_file( $template_file, $template_type );
$query_result[] = $template;
@@ -469,14 +459,6 @@ class BlockTemplatesController {
continue;
}
// At this point the template only exists in the Blocks filesystem, if is a taxonomy-product_cat/tag/attribute.html template
// let's use the archive-product.html template from Blocks.
if ( BlockTemplateUtils::template_is_eligible_for_product_archive_fallback( $template_slug ) ) {
$template_file = $this->get_template_path_from_woocommerce( ProductCatalogTemplate::SLUG );
$templates[] = BlockTemplateUtils::create_new_block_template_object( $template_file, $template_type, $template_slug, false );
continue;
}
// At this point the template only exists in the Blocks filesystem and has not been saved in the DB,
// or superseded by the theme.
$templates[] = BlockTemplateUtils::create_new_block_template_object( $template_file, $template_type, $template_slug );
@@ -500,18 +482,6 @@ class BlockTemplatesController {
return array_merge( $templates_from_db, $templates_from_woo );
}
/**
* Returns the path of a template on the Blocks template folder.
*
* @param string $template_slug Block template slug e.g. single-product.
* @param string $template_type wp_template or wp_template_part.
*
* @return string
*/
public function get_template_path_from_woocommerce( $template_slug, $template_type = 'wp_template' ) {
return BlockTemplateUtils::get_templates_directory( $template_type ) . '/' . $template_slug . '.html';
}
/**
* Checks whether a block template with that name exists in Woo Blocks
*

View File

@@ -19,6 +19,7 @@ use Automattic\WooCommerce\Blocks\Templates\ProductCategoryTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductTagTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductSearchResultsTemplate;
use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductFiltersTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductFiltersOverlayTemplate;
/**
@@ -59,10 +60,14 @@ class BlockTemplatesRegistry {
}
if ( BlockTemplateUtils::supports_block_templates( 'wp_template_part' ) ) {
$template_parts = array(
MiniCartTemplate::SLUG => new MiniCartTemplate(),
CheckoutHeaderTemplate::SLUG => new CheckoutHeaderTemplate(),
ProductFiltersOverlayTemplate::SLUG => new ProductFiltersOverlayTemplate(),
MiniCartTemplate::SLUG => new MiniCartTemplate(),
CheckoutHeaderTemplate::SLUG => new CheckoutHeaderTemplate(),
);
if ( Features::is_enabled( 'experimental-blocks' ) ) {
$template_parts[ ProductFiltersTemplate::SLUG ] = new ProductFiltersTemplate();
$template_parts[ ProductFiltersOverlayTemplate::SLUG ] = new ProductFiltersOverlayTemplate();
}
} else {
$template_parts = array();
}

View File

@@ -36,9 +36,9 @@ class Checkout extends AbstractBlock {
// 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 ) {
function ( $redirect_empty_cart ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return isset( $_GET['_wp-find-template'] ) ? false : $return;
return isset( $_GET['_wp-find-template'] ) ? false : $redirect_empty_cart;
}
);
@@ -94,10 +94,17 @@ class Checkout extends AbstractBlock {
* @return array|string
*/
protected function get_block_type_script( $key = null ) {
$dependencies = [];
// Load password strength meter script asynchronously if needed.
if ( ! is_user_logged_in() && 'no' === get_option( 'woocommerce_registration_generate_password' ) ) {
$dependencies[] = 'zxcvbn-async';
}
$script = [
'handle' => 'wc-' . $this->block_name . '-block-frontend',
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ),
'dependencies' => [],
'dependencies' => $dependencies,
];
return $key ? $script[ $key ] : $script;
}
@@ -354,6 +361,7 @@ class Checkout extends AbstractBlock {
$this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ) );
$this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ) );
$this->asset_data_registry->add( 'forcedBillingAddress', 'billing_only' === get_option( 'woocommerce_ship_to_destination' ) );
$this->asset_data_registry->add( 'generatePassword', filter_var( get_option( 'woocommerce_registration_generate_password' ), FILTER_VALIDATE_BOOLEAN ) );
$this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled() );
$this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled() );
$this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled() );
@@ -377,7 +385,7 @@ class Checkout extends AbstractBlock {
$shipping_methods = WC()->shipping()->get_shipping_methods();
$formatted_shipping_methods = array_reduce(
$shipping_methods,
function( $acc, $method ) {
function ( $acc, $method ) {
if ( in_array( $method->id, LocalPickupUtils::get_local_pickup_method_ids(), true ) ) {
return $acc;
}
@@ -405,7 +413,7 @@ class Checkout extends AbstractBlock {
$payment_methods = $this->get_enabled_payment_gateways();
$formatted_payment_methods = array_reduce(
$payment_methods,
function( $acc, $method ) {
function ( $acc, $method ) {
$acc[] = [
'id' => $method->id,
'title' => $method->method_title,
@@ -427,7 +435,7 @@ class Checkout extends AbstractBlock {
$all_plugins = \get_plugins(); // Note that `get_compatible_plugins_for_feature` calls `get_plugins` internally, so this is already in cache.
$incompatible_extensions = array_reduce(
$declared_extensions['incompatible'],
function( $acc, $item ) use ( $all_plugins ) {
function ( $acc, $item ) use ( $all_plugins ) {
$plugin = $all_plugins[ $item ] ?? null;
$plugin_id = $plugin['TextDomain'] ?? dirname( $item, 2 );
$plugin_name = $plugin['Name'] ?? $plugin_id;
@@ -465,7 +473,7 @@ class Checkout extends AbstractBlock {
$payment_gateways = WC()->payment_gateways->payment_gateways();
return array_filter(
$payment_gateways,
function( $payment_gateway ) {
function ( $payment_gateway ) {
return 'yes' === $payment_gateway->enabled;
}
);
@@ -500,7 +508,7 @@ class Checkout extends AbstractBlock {
$payment_methods[ $payment_method_group ] = array_values(
array_filter(
$saved_payment_methods,
function( $saved_payment_method ) use ( $payment_gateways ) {
function ( $saved_payment_method ) use ( $payment_gateways ) {
return in_array( $saved_payment_method['method']['gateway'], array_keys( $payment_gateways ), true );
}
)

View File

@@ -21,12 +21,24 @@ class ComingSoon extends AbstractBlock {
}
/**
* Get the frontend style handle for this block type.
* Enqueue frontend assets for this block, just in time for rendering.
*
* @return null
* @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 string $content The block content.
* @param WP_Block $block The block object.
*/
protected function get_block_type_style() {
return null;
protected function enqueue_assets( array $attributes, $content, $block ) {
parent::enqueue_assets( $attributes, $content, $block );
if ( isset( $attributes['color'] ) ) {
wp_add_inline_style(
'wc-blocks-style',
':root{--woocommerce-coming-soon-color: ' . esc_html( $attributes['color'] ) . '}'
);
}
}
/**

View File

@@ -11,9 +11,10 @@ use Automattic\WooCommerce\Blocks\Utils\BlockHooksTrait;
class CustomerAccount extends AbstractBlock {
use BlockHooksTrait;
const TEXT_ONLY = 'text_only';
const ICON_ONLY = 'icon_only';
const DISPLAY_ALT = 'alt';
const TEXT_ONLY = 'text_only';
const ICON_ONLY = 'icon_only';
const DISPLAY_ALT = 'alt';
const DISPLAY_LINE = 'line';
/**
* Block name.
@@ -33,6 +34,7 @@ class CustomerAccount extends AbstractBlock {
'anchor' => 'core/navigation',
'area' => 'header',
'callback' => 'should_unhook_block',
'version' => '8.4.0',
),
);
@@ -65,6 +67,7 @@ class CustomerAccount extends AbstractBlock {
*/
public function modify_hooked_block_attributes( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ) {
$parsed_hooked_block['attrs']['displayStyle'] = 'icon_only';
$parsed_hooked_block['attrs']['iconStyle'] = 'line';
/*
* The Mini Cart block (which is hooked into the header) has a margin of 0.5em on the left side.
@@ -116,16 +119,26 @@ class CustomerAccount extends AbstractBlock {
$account_link = get_option( 'woocommerce_myaccount_page_id' ) ? wc_get_account_endpoint_url( 'dashboard' ) : wp_login_url();
$allowed_svg = array(
'svg' => array(
'svg' => array(
'class' => true,
'xmlns' => true,
'width' => true,
'height' => true,
'viewbox' => true,
),
'path' => array(
'd' => true,
'fill' => true,
'path' => array(
'd' => true,
'fill' => true,
'fill-rule' => true,
'clip-rule' => true,
),
'circle' => array(
'cx' => true,
'cy' => true,
'r' => true,
'stroke' => true,
'stroke-width' => true,
'fill' => true,
),
);
@@ -153,8 +166,27 @@ class CustomerAccount extends AbstractBlock {
return '';
}
if ( self::DISPLAY_LINE === $attributes['iconStyle'] ) {
return '<svg class="' . $attributes['iconClass'] . '" viewBox="5 5 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle
cx="16"
cy="10.5"
r="3.5"
stroke="currentColor"
stroke-width="2"
fill="none"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.5 18.5H20.5C21.8807 18.5 23 19.6193 23 21V25.5H25V21C25 18.5147 22.9853 16.5 20.5 16.5H11.5C9.01472 16.5 7 18.5147 7 21V25.5H9V21C9 19.6193 10.1193 18.5 11.5 18.5Z"
fill="currentColor"
/>
</svg>';
}
if ( self::DISPLAY_ALT === $attributes['iconStyle'] ) {
return '<svg class="' . $attributes['iconClass'] . '" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" width="18" height="18">
return '<svg class="' . $attributes['iconClass'] . '" xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 19 19" width="18" height="18">
<path
d="M9 0C4.03579 0 0 4.03579 0 9C0 13.9642 4.03579 18 9 18C13.9642 18 18 13.9642 18 9C18 4.03579 13.9642 0 9
0ZM9 4.32C10.5347 4.32 11.7664 5.57056 11.7664 7.08638C11.7664 8.62109 10.5158 9.85277 9 9.85277C7.4653
@@ -166,7 +198,7 @@ class CustomerAccount extends AbstractBlock {
</svg>';
}
return '<svg class="' . $attributes['iconClass'] . '" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
return '<svg class="' . $attributes['iconClass'] . '" xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 17 17" width="16" height="16">
<path
d="M8.00009 8.34785C10.3096 8.34785 12.1819 6.47909 12.1819 4.17393C12.1819 1.86876 10.3096 0 8.00009 0C5.69055
0 3.81824 1.86876 3.81824 4.17393C3.81824 6.47909 5.69055 8.34785 8.00009 8.34785ZM0.333496 15.6522C0.333496

View File

@@ -65,6 +65,7 @@ class MiniCart extends AbstractBlock {
'position' => 'after',
'anchor' => 'core/navigation',
'area' => 'header',
'version' => '8.4.0',
),
);
@@ -459,7 +460,7 @@ class MiniCart extends AbstractBlock {
// It is not necessary to load the Mini-Cart Block on Cart and Checkout page.
return '<div class="' . esc_attr( $wrapper_classes ) . '" style="visibility:hidden" aria-hidden="true">
<button class="wc-block-mini-cart__button" disabled>' . $button_html . '</button>
<button class="wc-block-mini-cart__button" disabled aria-label="' . __( 'Cart', 'woocommerce' ) . '">' . $button_html . '</button>
</div>';
}
@@ -487,7 +488,7 @@ class MiniCart extends AbstractBlock {
}
return '<div class="' . esc_attr( $wrapper_classes ) . '" style="' . esc_attr( $wrapper_styles ) . '">
<button class="wc-block-mini-cart__button">' . $button_html . '</button>
<button class="wc-block-mini-cart__button" aria-label="' . __( 'Cart', 'woocommerce' ) . '">' . $button_html . '</button>
<div class="is-loading wc-block-components-drawer__screen-overlay wc-block-components-drawer__screen-overlay--is-hidden" aria-hidden="true">
<div class="wc-block-mini-cart__drawer wc-block-components-drawer">
<div class="wc-block-components-drawer__content">
@@ -546,7 +547,7 @@ class MiniCart extends AbstractBlock {
'tax_label' => $tax_label,
'display_cart_prices_including_tax' => false,
);
};
}
return array(
'tax_label' => '',

View File

@@ -40,13 +40,27 @@ class Status extends AbstractOrderConfirmationBlock {
return '';
}
$account_notice = $this->render_account_notice( $order );
if ( $account_notice ) {
$block = sprintf(
'<div class="wc-block-order-confirmation-status-notices %1$s">%2$s</div>',
esc_attr( trim( $classname ) ),
$account_notice
) . $block;
}
$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;
if ( $additional_content ) {
$block = $block . sprintf(
'<div class="wc-block-order-confirmation-status-description %1$s">%2$s</div>',
esc_attr( trim( $classname ) ),
$additional_content
);
}
return $block;
}
/**
@@ -142,6 +156,33 @@ class Status extends AbstractOrderConfirmationBlock {
return '<p>' . esc_html__( 'Please check your email for the order confirmation.', 'woocommerce' ) . '</p>';
}
/**
* If the user associated with the order needs to set a password (new account) show a notice.
*
* @param \WC_Order|null $order Order object.
* @return string
*/
protected function render_account_notice( $order = null ) {
if ( $order && $order->get_customer_id() && 'store-api' === $order->get_created_via() ) {
$nag = get_user_option( 'default_password_nag', $order->get_customer_id() );
$generate = filter_var( get_option( 'woocommerce_registration_generate_password', false ), FILTER_VALIDATE_BOOLEAN );
if ( $nag && $generate ) {
return wc_print_notice(
sprintf(
// translators: %s: site name.
__( 'Your account with %s has been successfully created. We emailed you a link to set your account password.', 'woocommerce' ),
esc_html( wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) )
),
'notice',
array(),
true
);
}
}
return '';
}
/**
* If the order is invalid or there is no permission to view the details, tell the user to check email or log-in.
*

View File

@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
use WP_Query;
use WC_Tax;
@@ -76,6 +77,9 @@ 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 );
// Provide location context into block's context.
add_filter( 'render_block_context', array( $this, 'provide_location_context_for_inner_blocks' ), 11, 1 );
// Interactivity API: Add navigation directives to the product collection block.
add_filter( 'render_block_woocommerce/product-collection', array( $this, 'enhance_product_collection_with_interactivity' ), 10, 2 );
add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
@@ -86,6 +90,71 @@ class ProductCollection extends AbstractBlock {
add_filter( 'render_block_data', array( $this, 'disable_enhanced_pagination' ), 10, 1 );
}
/**
* Provides the location context to each inner block of the product collection block.
* Hint: Only blocks using the 'query' context will be affected.
*
* The sourceData structure depends on the context type as follows:
* - site: [ ]
* - order: [ 'orderId' => int ]
* - cart: [ 'productIds' => int[] ]
* - archive: [ 'taxonomy' => string, 'termId' => int ]
* - product: [ 'productId' => int ]
*
* @example array(
* 'type' => 'product',
* 'sourceData' => array( 'productId' => 123 ),
* )
*
* @param array $context The block context.
* @return array $context {
* The block context including the product collection location context.
*
* @type array $productCollectionLocation {
* @type string $type The context type. Possible values are 'site', 'order', 'cart', 'archive', 'product'.
* @type array $sourceData The context source data. Can be the product ID of the viewed product, the order ID of the current order viewed, etc. See structure above for more details.
* }
* }
*/
public function provide_location_context_for_inner_blocks( $context ) {
// Run only on frontend.
// This is needed to avoid SSR renders while in editor. @see https://github.com/woocommerce/woocommerce/issues/45181.
if ( is_admin() || \WC()->is_rest_api_request() ) {
return $context;
}
// Target only product collection's inner blocks that use the 'query' context.
if ( ! isset( $context['query'] ) || ! isset( $context['query']['isProductCollectionBlock'] ) || ! $context['query']['isProductCollectionBlock'] ) {
return $context;
}
$is_in_single_product = isset( $context['singleProduct'] ) && ! empty( $context['postId'] );
$context['productCollectionLocation'] = $is_in_single_product ? array(
'type' => 'product',
'sourceData' => array(
'productId' => absint( $context['postId'] ),
),
) : $this->get_location_context();
return $context;
}
/**
* Get the global location context.
* Serve as a runtime cache for the location context.
*
* @see ProductCollectionUtils::parse_frontend_location_context()
*
* @return array The location context.
*/
private function get_location_context() {
static $location_context = null;
if ( null === $location_context ) {
$location_context = ProductCollectionUtils::parse_frontend_location_context();
}
return $location_context;
}
/**
* Enhances the Product Collection block with client-side pagination.
*
@@ -451,10 +520,14 @@ 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();
$is_exclude_applied_filters = ! ( $block->context['query']['inherit'] ?? false );
$inherit = $block->context['query']['inherit'] ?? false;
$filterable = $block->context['query']['filterable'] ?? false;
$is_exclude_applied_filters = ! ( $inherit || $filterable );
return $this->get_final_frontend_query( $block_context_query, $page, $is_exclude_applied_filters );
}

View File

@@ -39,8 +39,6 @@ class ProductFilters extends AbstractBlock {
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
return <<<HTML
<div>Product Filters</div>
HTML;
return $content;
}
}

View File

@@ -1,6 +1,9 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
* ProductFiltersOverlay class.
*/
@@ -30,10 +33,49 @@ class ProductFiltersOverlay extends AbstractBlock {
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
ob_start();
printf( '<div>%s</div>', esc_html__( 'Filters Overlay', 'woocommerce' ) );
$html = ob_get_clean();
return $content;
}
return $html;
/**
* 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 );
$template_part_edit_uri = '';
if (
current_user_can( 'edit_theme_options' ) &&
( wc_current_theme_is_fse_theme() || current_theme_supports( 'block-template-parts' ) )
) {
$theme_slug = BlockTemplateUtils::theme_has_template_part( 'product-filters-overlay' ) ? wp_get_theme()->get_stylesheet() : BlockTemplateUtils::PLUGIN_SLUG;
$site_editor_uri = add_query_arg(
array(
'canvas' => 'edit',
'path' => '/template-parts/single',
),
admin_url( 'site-editor.php' )
);
$template_part_edit_uri = esc_url_raw(
add_query_arg(
array(
'postId' => sprintf( '%s//%s', $theme_slug, 'product-filters-overlay' ),
'postType' => 'wp_template_part',
),
$site_editor_uri
)
);
}
$this->asset_data_registry->add(
'templatePartProductFiltersOverlayEditUri',
$template_part_edit_uri
);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* ProductFilters class.
*/
class ProductFiltersOverlayNavigation extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-filters-overlay-navigation';
}

View File

@@ -48,6 +48,8 @@ class ProductTemplate extends AbstractBlock {
$classnames = '';
if ( isset( $block->context['displayLayout'] ) && isset( $block->context['query'] ) ) {
$classnames = 'is-product-collection-layout-' . $block->context['displayLayout']['type'] . ' ';
if ( isset( $block->context['displayLayout']['type'] ) && 'flex' === $block->context['displayLayout']['type'] ) {
if ( isset( $block->context['displayLayout']['shrinkColumns'] ) && $block->context['displayLayout']['shrinkColumns'] ) {
$classnames = "wc-block-product-template__responsive columns-{$block->context['displayLayout']['columns']}";
@@ -56,6 +58,7 @@ class ProductTemplate extends AbstractBlock {
}
}
}
if ( isset( $attributes['style']['elements']['link']['color']['text'] ) ) {
$classnames .= ' has-link-color';
}

View File

@@ -31,6 +31,13 @@ final class BlockTypesController {
*/
protected $asset_data_registry;
/**
* Holds the registered blocks that have WooCommerce blocks as their parents.
*
* @var array List of registered blocks.
*/
private $registered_blocks_with_woocommerce_parents;
/**
* Constructor.
*
@@ -67,6 +74,45 @@ final class BlockTypesController {
);
}
/**
* Get registered blocks that have WooCommerce blocks as their parents. Adds the value to the
* `registered_blocks_with_woocommerce_parents` cache if `init` has been fired.
*
* @return array Registered blocks with WooCommerce blocks as parents.
*/
public function get_registered_blocks_with_woocommerce_parent() {
// If init has run and the cache is already set, return it.
if ( did_action( 'init' ) && ! empty( $this->registered_blocks_with_woocommerce_parents ) ) {
return $this->registered_blocks_with_woocommerce_parents;
}
$registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered();
if ( ! is_array( $registered_blocks ) ) {
return array();
}
$this->registered_blocks_with_woocommerce_parents = array_filter(
$registered_blocks,
function ( $block ) {
if ( empty( $block->parent ) ) {
return false;
}
if ( ! is_array( $block->parent ) ) {
$block->parent = array( $block->parent );
}
$woocommerce_blocks = array_filter(
$block->parent,
function ( $parent_block_name ) {
return 'woocommerce' === strtok( $parent_block_name, '/' );
}
);
return ! empty( $woocommerce_blocks );
}
);
return $this->registered_blocks_with_woocommerce_parents;
}
/**
* Check if the current post has a block with a specific attribute value.
*
@@ -132,14 +178,15 @@ final class BlockTypesController {
}
/**
* Add data- attributes to blocks when rendered if the block is under the woocommerce/ namespace.
* Check if a block should have data attributes appended on render. If it's in an allowed namespace, or the block
* has explicitly been added to the allowed block list, or if one of the block's parents is in the WooCommerce
* namespace it can have data attributes.
*
* @param string $content Block content.
* @param array $block Parsed block data.
* @return string
* @param string $block_name Name of the block to check.
*
* @return boolean
*/
public function add_data_attributes( $content, $block ) {
$block_name = $block['blockName'];
public function block_should_have_data_attributes( $block_name ) {
$block_namespace = strtok( $block_name ?? '', '/' );
/**
@@ -164,18 +211,44 @@ final class BlockTypesController {
*/
$allowed_blocks = (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_block', array() );
if ( ! in_array( $block_namespace, $allowed_namespaces, true ) && ! in_array( $block_name, $allowed_blocks, true ) ) {
$blocks_with_woo_parents = $this->get_registered_blocks_with_woocommerce_parent();
$block_has_woo_parent = in_array( $block_name, array_keys( $blocks_with_woo_parents ), true );
$in_allowed_namespace_list = in_array( $block_namespace, $allowed_namespaces, true );
$in_allowed_block_list = in_array( $block_name, $allowed_blocks, true );
return $block_has_woo_parent || $in_allowed_block_list || $in_allowed_namespace_list;
}
/**
* Add data- attributes to blocks when rendered if the block is under the woocommerce/ namespace.
*
* @param string $content Block content.
* @param array $block Parsed block data.
* @return string
*/
public function add_data_attributes( $content, $block ) {
$content = trim( $content );
if ( ! $this->block_should_have_data_attributes( $block['blockName'] ) ) {
return $content;
}
$attributes = (array) $block['attrs'];
$exclude_attributes = array( 'className', 'align' );
$escaped_data_attributes = array(
'data-block-name="' . esc_attr( $block['blockName'] ) . '"',
);
$attributes = (array) $block['attrs'];
$exclude_attributes = array( 'className', 'align' );
foreach ( $attributes as $key => $value ) {
if ( in_array( $key, $exclude_attributes, true ) ) {
$processor = new \WP_HTML_Tag_Processor( $content );
if (
false === $processor->next_token() ||
'DIV' !== $processor->get_token_name() ||
$processor->is_tag_closer()
) {
return $content;
}
foreach ( $attributes as $key => $value ) {
if ( ! is_string( $key ) || in_array( $key, $exclude_attributes, true ) ) {
continue;
}
if ( is_bool( $value ) ) {
@@ -184,10 +257,16 @@ final class BlockTypesController {
if ( ! is_scalar( $value ) ) {
$value = wp_json_encode( $value );
}
$escaped_data_attributes[] = 'data-' . esc_attr( strtolower( preg_replace( '/(?<!\ )[A-Z]/', '-$0', $key ) ) ) . '="' . esc_attr( $value ) . '"';
// For output consistency, we convert camelCase to kebab-case and output in lowercase.
$key = strtolower( preg_replace( '/(?<!^|\ )[A-Z]/', '-$0', $key ) );
$processor->set_attribute( "data-{$key}", $value );
}
return preg_replace( '/^<div /', '<div ' . implode( ' ', $escaped_data_attributes ) . ' ', trim( $content ) );
// Set this last to prevent user-input from overriding it.
$processor->set_attribute( 'data-block-name', $block['blockName'] );
return $processor->get_updated_html();
}
/**
@@ -327,6 +406,7 @@ final class BlockTypesController {
$block_types[] = 'ProductFilter';
$block_types[] = 'ProductFilters';
$block_types[] = 'ProductFiltersOverlay';
$block_types[] = 'ProductFiltersOverlayNavigation';
$block_types[] = 'ProductFilterStockStatus';
$block_types[] = 'ProductFilterPrice';
$block_types[] = 'ProductFilterAttribute';

View File

@@ -41,6 +41,8 @@ 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;
use Automattic\WooCommerce\Blocks\TemplateOptions;
/**
* Takes care of bootstrapping the plugin.
@@ -122,9 +124,23 @@ class Bootstrap {
0
);
$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/' ) );
// We need to initialize BlockTemplatesController and BlockTemplatesRegistry at the end of `after_setup_theme`
// so themes had the opportunity to declare support for template parts.
add_action(
'after_setup_theme',
function () {
$is_store_api_request = wc()->is_store_api_request();
if ( ! $is_store_api_request && ( wc_current_theme_is_fse_theme() || current_theme_supports( 'block-template-parts' ) ) ) {
$this->container->get( BlockTemplatesRegistry::class )->init();
$this->container->get( BlockTemplatesController::class )->init();
}
},
999
);
$is_rest = wc()->is_rest_api_request();
$is_store_api_request = wc()->is_store_api_request();
// Load and init assets.
$this->container->get( StoreApi::class )->init();
@@ -152,13 +168,12 @@ class Bootstrap {
$this->container->get( AIPatterns::class );
$this->container->get( BlockPatterns::class );
$this->container->get( BlockTypesController::class );
$this->container->get( BlockTemplatesRegistry::class )->init();
$this->container->get( BlockTemplatesController::class )->init();
$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( PTKPatternsStore::class );
$this->container->get( TemplateOptions::class )->init();
}
$this->container->get( QueryFilters::class )->init();
@@ -254,18 +269,6 @@ class Bootstrap {
return new BlockTypesController( $asset_api, $asset_data_registry );
}
);
$this->container->register(
BlockTemplatesRegistry::class,
function () {
return new BlockTemplatesRegistry();
}
);
$this->container->register(
BlockTemplatesController::class,
function () {
return new BlockTemplatesController();
}
);
$this->container->register(
ClassicTemplatesCompatibility::class,
function ( Container $container ) {
@@ -350,6 +353,12 @@ class Bootstrap {
return new StoreApi();
}
);
$this->container->register(
TemplateOptions::class,
function () {
return new TemplateOptions();
}
);
// Maintains backwards compatibility with previous Store API namespace.
$this->container->register(
'Automattic\WooCommerce\Blocks\StoreApi\Formatters',
@@ -427,6 +436,18 @@ class Bootstrap {
return new QueryFilters();
}
);
$this->container->register(
BlockTemplatesRegistry::class,
function () {
return new BlockTemplatesRegistry();
}
);
$this->container->register(
BlockTemplatesController::class,
function () {
return new BlockTemplatesController();
}
);
}
/**

View File

@@ -13,13 +13,6 @@ use WP_Error;
*/
class CheckoutFields {
/**
* Core checkout fields.
*
* @var array
*/
private $core_fields;
/**
* Additional checkout fields.
*
@@ -91,144 +84,10 @@ class CheckoutFields {
*/
public function __construct( AssetDataRegistry $asset_data_registry ) {
$this->asset_data_registry = $asset_data_registry;
$this->core_fields = [
'email' => [
'label' => __( 'Email address', 'woocommerce' ),
'optionalLabel' => __(
'Email address (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'email',
'autocapitalize' => 'none',
'index' => 0,
],
'country' => [
'label' => __( 'Country/Region', 'woocommerce' ),
'optionalLabel' => __(
'Country/Region (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'country',
'index' => 1,
],
'first_name' => [
'label' => __( 'First name', 'woocommerce' ),
'optionalLabel' => __(
'First name (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'given-name',
'autocapitalize' => 'sentences',
'index' => 10,
],
'last_name' => [
'label' => __( 'Last name', 'woocommerce' ),
'optionalLabel' => __(
'Last name (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'family-name',
'autocapitalize' => 'sentences',
'index' => 20,
],
'company' => [
'label' => __( 'Company', 'woocommerce' ),
'optionalLabel' => __(
'Company (optional)',
'woocommerce'
),
'required' => false,
'hidden' => false,
'autocomplete' => 'organization',
'autocapitalize' => 'sentences',
'index' => 30,
],
'address_1' => [
'label' => __( 'Address', 'woocommerce' ),
'optionalLabel' => __(
'Address (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'address-line1',
'autocapitalize' => 'sentences',
'index' => 40,
],
'address_2' => [
'label' => __( 'Apartment, suite, etc.', 'woocommerce' ),
'optionalLabel' => __(
'Apartment, suite, etc. (optional)',
'woocommerce'
),
'required' => false,
'hidden' => false,
'autocomplete' => 'address-line2',
'autocapitalize' => 'sentences',
'index' => 50,
],
'city' => [
'label' => __( 'City', 'woocommerce' ),
'optionalLabel' => __(
'City (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'address-level2',
'autocapitalize' => 'sentences',
'index' => 70,
],
'state' => [
'label' => __( 'State/County', 'woocommerce' ),
'optionalLabel' => __(
'State/County (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'address-level1',
'autocapitalize' => 'sentences',
'index' => 80,
],
'postcode' => [
'label' => __( 'Postal code', 'woocommerce' ),
'optionalLabel' => __(
'Postal code (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'postal-code',
'autocapitalize' => 'characters',
'index' => 90,
],
'phone' => [
'label' => __( 'Phone', 'woocommerce' ),
'optionalLabel' => __(
'Phone (optional)',
'woocommerce'
),
'required' => false,
'hidden' => false,
'type' => 'tel',
'autocomplete' => 'tel',
'autocapitalize' => 'characters',
'index' => 100,
],
];
$this->fields_locations = [
// omit email from shipping and billing fields.
'address' => array_merge( \array_diff_key( array_keys( $this->core_fields ), array( 'email' ) ) ),
'address' => array_merge( \array_diff_key( $this->get_core_fields_keys(), array( 'email' ) ) ),
'contact' => array( 'email' ),
'order' => [],
];
@@ -527,17 +386,8 @@ class CheckoutFields {
$field_data['options'] = $cleaned_options;
// If the field is not required, inject an empty option at the start.
if ( isset( $field_data['required'] ) && false === $field_data['required'] && ! in_array( '', $added_values, true ) ) {
$field_data['options'] = array_merge(
[
[
'value' => '',
'label' => '',
],
],
$field_data['options']
);
if ( isset( $field_data['placeholder'] ) ) {
$field_data['placeholder'] = sanitize_text_field( $field_data['placeholder'] );
}
return $field_data;
@@ -620,13 +470,167 @@ class CheckoutFields {
);
}
/**
* Returns the keys of all core fields.
*
* @return array An array of field keys.
*/
public function get_core_fields_keys() {
return [
'email',
'country',
'first_name',
'last_name',
'company',
'address_1',
'address_2',
'city',
'state',
'postcode',
'phone',
];
}
/**
* Returns an array of all core fields.
*
* @return array An array of fields.
*/
public function get_core_fields() {
return $this->core_fields;
return [
'email' => [
'label' => __( 'Email address', 'woocommerce' ),
'optionalLabel' => __(
'Email address (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'email',
'autocapitalize' => 'none',
'index' => 0,
],
'country' => [
'label' => __( 'Country/Region', 'woocommerce' ),
'optionalLabel' => __(
'Country/Region (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'country',
'index' => 1,
],
'first_name' => [
'label' => __( 'First name', 'woocommerce' ),
'optionalLabel' => __(
'First name (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'given-name',
'autocapitalize' => 'sentences',
'index' => 10,
],
'last_name' => [
'label' => __( 'Last name', 'woocommerce' ),
'optionalLabel' => __(
'Last name (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'family-name',
'autocapitalize' => 'sentences',
'index' => 20,
],
'company' => [
'label' => __( 'Company', 'woocommerce' ),
'optionalLabel' => __(
'Company (optional)',
'woocommerce'
),
'required' => false,
'hidden' => false,
'autocomplete' => 'organization',
'autocapitalize' => 'sentences',
'index' => 30,
],
'address_1' => [
'label' => __( 'Address', 'woocommerce' ),
'optionalLabel' => __(
'Address (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'address-line1',
'autocapitalize' => 'sentences',
'index' => 40,
],
'address_2' => [
'label' => __( 'Apartment, suite, etc.', 'woocommerce' ),
'optionalLabel' => __(
'Apartment, suite, etc. (optional)',
'woocommerce'
),
'required' => false,
'hidden' => false,
'autocomplete' => 'address-line2',
'autocapitalize' => 'sentences',
'index' => 50,
],
'city' => [
'label' => __( 'City', 'woocommerce' ),
'optionalLabel' => __(
'City (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'address-level2',
'autocapitalize' => 'sentences',
'index' => 70,
],
'state' => [
'label' => __( 'State/County', 'woocommerce' ),
'optionalLabel' => __(
'State/County (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'address-level1',
'autocapitalize' => 'sentences',
'index' => 80,
],
'postcode' => [
'label' => __( 'Postal code', 'woocommerce' ),
'optionalLabel' => __(
'Postal code (optional)',
'woocommerce'
),
'required' => true,
'hidden' => false,
'autocomplete' => 'postal-code',
'autocapitalize' => 'characters',
'index' => 90,
],
'phone' => [
'label' => __( 'Phone', 'woocommerce' ),
'optionalLabel' => __(
'Phone (optional)',
'woocommerce'
),
'required' => false,
'hidden' => false,
'type' => 'tel',
'autocomplete' => 'tel',
'autocapitalize' => 'characters',
'index' => 100,
],
];
}
/**

View File

@@ -43,7 +43,7 @@ class Notices {
public function init() {
add_action(
'after_setup_theme',
function() {
function () {
/**
* Allow classic theme developers to opt-in to using block notices.
*
@@ -104,13 +104,14 @@ class Notices {
* @return string
*/
public function get_notices_template( $template, $template_name, $args, $template_path, $default_path ) {
$directory = get_stylesheet_directory();
$file = $directory . '/woocommerce/' . $template_name;
if ( file_exists( $file ) ) {
return $file;
}
if ( in_array( $template_name, $this->notice_templates, true ) ) {
$directory = get_stylesheet_directory();
$file = $directory . '/woocommerce/' . $template_name;
if ( file_exists( $file ) ) {
return $file;
}
$template = $this->package->get_path( 'templates/block-' . $template_name );
wp_enqueue_style( 'wc-blocks-style' );
}

View File

@@ -11,10 +11,9 @@ use WP_Upgrader;
class PTKPatternsStore {
const TRANSIENT_NAME = 'ptk_patterns';
// Some patterns need to be excluded because they have dependencies which
// are not installed by default (like Jetpack). Otherwise, the user
// would see an error when trying to insert them in the editor.
const EXCLUDED_PATTERNS = array( '13923', '14781', '14779', '13666', '13664', '13660', '13588', '14922', '14880', '13596', '13967', '13958', '15050', '15027' );
const CATEGORY_MAPPING = array(
'testimonials' => 'reviews',
);
/**
* PatternsToolkit instance.
@@ -92,11 +91,13 @@ class PTKPatternsStore {
* @return void
*/
private function schedule_action_if_not_pending( $action ) {
if ( as_has_scheduled_action( $action ) ) {
$last_request = get_transient( 'last_fetch_patterns_request' );
if ( as_has_scheduled_action( $action ) || false !== $last_request ) {
return;
}
as_schedule_single_action( time(), $action );
set_transient( 'last_fetch_patterns_request', time(), HOUR_IN_SECONDS );
}
/**
@@ -117,26 +118,31 @@ class PTKPatternsStore {
}
/**
* Filter patterns to exclude those with the given IDs.
* Filter the patterns that have external dependencies.
*
* @param array $patterns The patterns to filter.
* @param array $pattern_ids The pattern IDs to exclude.
* @return array
*/
private function filter_patterns( array $patterns, array $pattern_ids ) {
return array_filter(
$patterns,
function ( $pattern ) use ( $pattern_ids ) {
if ( ! isset( $pattern['ID'] ) ) {
private function filter_patterns( array $patterns ) {
return array_values(
array_filter(
$patterns,
function ( $pattern ) {
if ( ! isset( $pattern['ID'] ) ) {
return true;
}
if ( isset( $pattern['post_type'] ) && 'wp_block' !== $pattern['post_type'] ) {
return false;
}
if ( $this->has_external_dependencies( $pattern ) ) {
return false;
}
return true;
}
if ( isset( $pattern['post_type'] ) && 'wp_block' !== $pattern['post_type'] ) {
return false;
}
return ! in_array( (string) $pattern['ID'], $pattern_ids, true );
}
)
);
}
@@ -181,22 +187,35 @@ class PTKPatternsStore {
$patterns = $this->ptk_client->fetch_patterns(
array(
'categories' => array( 'intro', 'about', 'services', 'testimonials' ),
// This is the site where the patterns are stored. Despite the 'wpcomstaging.com' domain suggesting a staging environment, this URL points to the production environment where stable versions of the patterns are maintained.
'site' => 'wooblockpatterns.wpcomstaging.com',
'categories' => array(
'_woo_intro',
'_woo_featured_selling',
'_woo_about',
'_woo_reviews',
'_woo_social_media',
'_woo_woocommerce',
'_dotcom_imported_intro',
'_dotcom_imported_about',
'_dotcom_imported_services',
'_dotcom_imported_reviews',
),
)
);
if ( is_wp_error( $patterns ) ) {
wc_get_logger()->warning(
sprintf(
// translators: %s is a generated error message.
__( 'Failed to get the patterns from the PTK: "%s"', 'woocommerce' ),
__( 'Failed to get WooCommerce patterns from the PTK: "%s"', 'woocommerce' ),
$patterns->get_error_message()
),
);
return;
}
$patterns = $this->filter_patterns( $patterns, self::EXCLUDED_PATTERNS );
$patterns = $this->filter_patterns( $patterns );
$patterns = $this->map_categories( $patterns );
set_transient( self::TRANSIENT_NAME, $patterns );
}
@@ -209,4 +228,51 @@ class PTKPatternsStore {
private function allowed_tracking_is_enabled(): bool {
return 'yes' === get_option( 'woocommerce_allow_tracking' );
}
/**
* Change the categories of the patterns to match the ones used in the CYS flow
*
* @param array $patterns The patterns to map categories for.
* @return array The patterns with the categories mapped.
*/
private function map_categories( array $patterns ) {
return array_map(
function ( $pattern ) {
if ( isset( $pattern['categories'] ) ) {
foreach ( $pattern['categories'] as $key => $category ) {
if ( isset( $category['slug'] ) && isset( self::CATEGORY_MAPPING[ $key ] ) ) {
$new_category = self::CATEGORY_MAPPING[ $key ];
unset( $pattern['categories'][ $key ] );
$pattern['categories'][ $new_category ]['slug'] = $new_category;
$pattern['categories'][ $new_category ]['title'] = ucfirst( $new_category );
}
}
}
return $pattern;
},
$patterns
);
}
/**
* Check if the pattern has external dependencies.
*
* @param array $pattern The pattern to check.
*
* @return bool
*/
private function has_external_dependencies( $pattern ) {
if ( ! isset( $pattern['dependencies'] ) || ! is_array( $pattern['dependencies'] ) ) {
return false;
}
foreach ( $pattern['dependencies'] as $dependency ) {
if ( 'woocommerce' !== $dependency ) {
return true;
}
}
return false;
}
}

View File

@@ -10,6 +10,30 @@ class PatternRegistry {
const SLUG_REGEX = '/^[A-z0-9\/_-]+$/';
const COMMA_SEPARATED_REGEX = '/[\s,]+/';
/**
* Associates pattern slugs with their localized labels for categorization.
* Each key represents a unique pattern slug, while the value is the localized label.
*
* @var array $category_labels
*/
private $category_labels;
/**
* Constructor.
*/
public function __construct() {
$this->category_labels = [
'woo-commerce' => __( 'WooCommerce', 'woocommerce' ),
'intro' => __( 'Intro', 'woocommerce' ),
'featured-selling' => __( 'Featured Selling', 'woocommerce' ),
'about' => __( 'About', 'woocommerce' ),
'social-media' => __( 'Social Media', 'woocommerce' ),
'services' => __( 'Services', 'woocommerce' ),
'reviews' => __( 'Reviews', 'woocommerce' ),
];
}
/**
* Register a block pattern.
*
@@ -166,10 +190,13 @@ class PatternRegistry {
$pattern_data['categories'][ $key ] = $category_slug;
$label = isset( $this->category_labels[ $category_slug ] ) ? $this->category_labels[ $category_slug ] : self::kebab_to_capital_case( $category_slug );
register_block_pattern_category(
$category_slug,
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
array( 'label' => __( $category, 'woocommerce' ) )
array(
'label' => $label,
),
);
}
}
@@ -194,4 +221,18 @@ class PatternRegistry {
return null;
}
/**
* Convert a kebab-case string to capital case.
*
* @param string $value The kebab-case string.
*
* @return string
*/
private static function kebab_to_capital_case( $value ) {
$string = str_replace( '-', ' ', $value );
$string = ucwords( $string );
return $string;
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Blocks\Options;
/**
* TemplateOptions class.
*
* @internal
*/
class TemplateOptions {
/**
* Initialization method.
*/
public function init() {
add_action( 'after_switch_theme', array( $this, 'check_should_use_blockified_product_grid_templates' ), 10, 2 );
}
/**
* Checks the old and current themes and determines if the "wc_blocks_use_blockified_product_grid_block_as_template"
* option need to be updated accordingly.
*
* @param string $old_name Old theme name.
* @param \WP_Theme $old_theme Instance of the old theme.
* @return void
*/
public function check_should_use_blockified_product_grid_templates( $old_name, $old_theme ) {
if ( ! $old_theme->is_block_theme() && wc_current_theme_is_fse_theme() ) {
$option_name = Options::WC_BLOCK_USE_BLOCKIFIED_PRODUCT_GRID_BLOCK_AS_TEMPLATE;
// We previously stored "yes" or "no" values. This will convert them to true or false.
$option_value = wc_string_to_bool( get_option( $option_name ) );
// We don't need to do anything if the option is already set to true.
if ( ! $option_value ) {
update_option( $option_name, true );
}
}
}
}

View File

@@ -20,14 +20,12 @@ class ProductFiltersOverlayTemplate extends AbstractTemplatePart {
*
* @var string
*/
public $template_area = 'product-filters-overlay';
public $template_area = 'uncategorized';
/**
* Initialization method.
*/
public function init() {
add_filter( 'default_wp_template_part_areas', array( $this, 'register_product_filters_overlay_template_part_area' ), 10, 1 );
}
public function init() {}
/**
* Returns the title of the template.
@@ -46,21 +44,4 @@ class ProductFiltersOverlayTemplate extends AbstractTemplatePart {
public function get_template_description() {
return __( 'Template used to display the Product Filters Overlay.', 'woocommerce' );
}
/**
* Add Filters Overlay 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 Filters Overlay one.
*/
public function register_product_filters_overlay_template_part_area( $default_area_definitions ) {
$product_filters_overlay_template_part_area = array(
'area' => 'product-filters-overlay',
'label' => $this->get_template_title(),
'description' => $this->get_template_description(),
'icon' => 'filter',
'area_tag' => 'product-filters-overlay',
);
return array_merge( $default_area_definitions, array( $product_filters_overlay_template_part_area ) );
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\Templates;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
* ProductFiltersTemplate class.
*
* @internal
*/
class ProductFiltersTemplate extends AbstractTemplatePart {
/**
* The slug of the template.
*
* @var string
*/
const SLUG = 'product-filters';
/**
* The template part area where the template part belongs.
*
* @var string
*/
public $template_area = 'uncategorized';
/**
* Initialization method.
*/
public function init() {
add_filter( 'get_block_type_variations', array( $this, 'register_block_type_variation' ), 10, 2 );
}
/**
* Returns the title of the template.
*
* @return string
*/
public function get_template_title() {
return _x( 'Product Filters (Experimental)', 'Template name', 'woocommerce' );
}
/**
* Returns the description of the template.
*
* @return string
*/
public function get_template_description() {
return __(
'This is the template part for the product filters displayed on different pages across your store.',
'woocommerce'
);
}
/**
* Add variation for this template part to make it available in the block inserter.
*
* @param array $variations Array of registered variations for a block type.
* @param WP_Block_Type $block_type The full block type object.
*/
public function register_block_type_variation( $variations, $block_type ) {
if ( 'core/template-part' !== $block_type->name ) {
return $variations;
}
// If template part is modified, Core will pick it up and register a variation
// for it. Check if the variation already exists before adding it.
foreach ( $variations as $variation ) {
if ( ! empty( $variation['attributes']['slug'] ) && 'product-filters' === $variation['attributes']['slug'] ) {
return $variations;
}
}
$theme = 'woocommerce/woocommerce';
// Check if current theme overrides this template part.
if ( BlockTemplateUtils::theme_has_template_part( 'product-filters' ) ) {
$theme = wp_get_theme()->get( 'TextDomain' );
}
$variations[] = array(
'name' => 'file_' . self::SLUG,
'title' => $this->get_template_title(),
'description' => true,
'attributes' => array(
'slug' => self::SLUG,
'theme' => $theme,
'area' => $this->template_area,
),
'scope' => array( 'inserter' ),
'icon' => 'layout',
);
return $variations;
}
}

View File

@@ -17,13 +17,6 @@ class ProductSearchResultsTemplate extends AbstractTemplate {
*/
const SLUG = 'product-search-results';
/**
* The template used as a fallback if that one is customized.
*
* @var string
*/
public $fallback_template = ProductCatalogTemplate::SLUG;
/**
* Initialization method.
*/

View File

@@ -18,31 +18,34 @@ trait BlockHooksTrait {
* @return array An array of block slugs hooked into a given context.
*/
public function register_hooked_block( $hooked_blocks, $position, $anchor_block, $context ) {
/**
* If the block has no hook placements, return early.
*/
// If the block has no hook placements, return early.
if ( ! isset( $this->hooked_block_placements ) || empty( $this->hooked_block_placements ) ) {
return $hooked_blocks;
}
// Cache for active theme.
static $active_theme_name = null;
if ( is_null( $active_theme_name ) ) {
$active_theme_name = wp_get_theme()->get( 'Name' );
// Cache the block hooks version.
static $block_hooks_version = null;
if ( defined( 'WP_RUN_CORE_TESTS' ) || is_null( $block_hooks_version ) ) {
$block_hooks_version = get_option( 'woocommerce_hooked_blocks_version' );
}
/**
* A list of theme slugs to execute this with. This is a temporary
* measure until improvements to the Block Hooks API allow for exposing
* to all block themes.
*
* @since 8.4.0
*/
$theme_include_list = apply_filters( 'woocommerce_hooked_blocks_theme_include_list', array( 'Twenty Twenty-Four', 'Twenty Twenty-Three', 'Twenty Twenty-Two', 'Tsubaki', 'Zaino', 'Thriving Artist', 'Amulet', 'Tazza' ) );
// If block hooks are disabled or the version is not set, return early.
if ( 'no' === $block_hooks_version || false === $block_hooks_version ) {
return $hooked_blocks;
}
if ( $context && in_array( $active_theme_name, $theme_include_list, true ) ) {
foreach ( $this->hooked_block_placements as $placement ) {
// Valid placements are those that have no version specified,
// or have a version that is less than or equal to version specified in the woocommerce_hooked_blocks_version option.
$valid_placements = array_filter(
$this->hooked_block_placements,
function ( $placement ) use ( $block_hooks_version ) {
$placement_version = isset( $placement['version'] ) ? $placement['version'] : null;
return is_null( $placement_version ) || ! is_null( $placement_version ) && version_compare( $block_hooks_version, $placement_version, '>=' );
}
);
if ( $context && ! empty( $valid_placements ) ) {
foreach ( $valid_placements as $placement ) {
if ( $placement['position'] === $position && $placement['anchor'] === $anchor_block ) {
// If an area has been specified for this placement.

View File

@@ -316,9 +316,13 @@ class BlockTemplateUtils {
$wp_template_part_filenames = array(
'checkout-header.html',
'mini-cart.html',
'product-filters-overlay.html',
);
if ( Features::is_enabled( 'experimental-blocks' ) ) {
$wp_template_part_filenames[] = 'product-filters.html';
$wp_template_part_filenames[] = 'product-filters-overlay.html';
}
/*
* This may return the blockified directory for wp_templates.
* At the moment every template file has a corresponding blockified file.

View File

@@ -8,6 +8,7 @@ use WP_Query;
* {@internal This class and its methods are not intended for public use.}
*/
class ProductCollectionUtils {
/**
* Prepare and execute a query for the Product Collection block.
* This method is used by the Product Collection block and the No Results block.
@@ -78,6 +79,87 @@ class ProductCollectionUtils {
return self::remove_empty_array_recursive( $queries );
}
/**
* Parse WP Query's front-end context for the Product Collection block.
*
* The sourceData structure depends on the context type as follows:
* - site: [ ]
* - order: [ 'orderId' => int ]
* - cart: [ 'productIds' => int[] ]
* - archive: [ 'taxonomy' => string, 'termId' => int ]
* - product: [ 'productId' => int ]
*
* @return array $context {
* @type string $type The context type. Possible values are 'site', 'order', 'cart', 'archive', 'product'.
* @type array $sourceData The context source data. Can be the product ID of the viewed product, the order ID of the current order, etc.
* }
*/
public static function parse_frontend_location_context() {
global $wp_query;
// Default context.
// Hint: The Shop page uses the default context.
$type = 'site';
$source_data = array();
if ( ! ( $wp_query instanceof WP_Query ) ) {
return array(
'type' => $type,
'sourceData' => $source_data,
);
}
// As more areas are blockified, expected future contexts include:
// - is_checkout_pay_page().
// - is_view_order_page().
if ( is_order_received_page() ) {
$type = 'order';
$source_data = array( 'orderId' => absint( $wp_query->query_vars['order-received'] ) );
} elseif ( ( is_cart() || is_checkout() ) && isset( WC()->cart ) && is_a( WC()->cart, 'WC_Cart' ) ) {
$type = 'cart';
$items = array();
foreach ( WC()->cart->get_cart() as $cart_item ) {
if ( ! isset( $cart_item['product_id'] ) ) {
continue;
}
$items[] = absint( $cart_item['product_id'] );
}
$items = array_unique( array_filter( $items ) );
$source_data = array( 'productIds' => $items );
} elseif ( is_product_taxonomy() ) {
$source = $wp_query->get_queried_object();
$is_valid = is_a( $source, 'WP_Term' );
$taxonomy = $is_valid ? $source->taxonomy : '';
$term_id = $is_valid ? $source->term_id : '';
$type = 'archive';
$source_data = array(
'taxonomy' => wc_clean( $taxonomy ),
'termId' => absint( $term_id ),
);
} elseif ( is_product() ) {
$source = $wp_query->get_queried_object();
$product_id = is_a( $source, 'WP_Post' ) ? absint( $source->ID ) : 0;
$type = 'product';
$source_data = array( 'productId' => $product_id );
}
$context = array(
'type' => $type,
'sourceData' => $source_data,
);
return $context;
}
/**
* Remove falsy item from array, recursively.
*

View File

@@ -88,7 +88,7 @@ final class ReserveStock {
try {
$items = array_filter(
$order->get_items(),
function( $item ) {
function ( $item ) {
return $item->is_type( 'line_item' ) && $item->get_product() instanceof \WC_Product && $item->get_quantity() > 0;
}
);

View File

@@ -1,12 +1,13 @@
<?php
namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable;
namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\DataStores\Orders\LegacyDataHandler;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Utilities\PluginUtil;
use WP_CLI;
/**
@@ -76,6 +77,7 @@ class CLIRunner {
WP_CLI::add_command( 'wc hpos status', array( $this, 'status' ) );
WP_CLI::add_command( 'wc hpos diff', array( $this, 'diff' ) );
WP_CLI::add_command( 'wc hpos backfill', array( $this, 'backfill' ) );
WP_CLI::add_command( 'wc hpos compatibility-info', array( $this, 'compatibility_info' ) );
WP_CLI::add_command( 'wc hpos compatibility-mode enable', array( $this, 'enable_compat_mode' ) );
WP_CLI::add_command( 'wc hpos compatibility-mode disable', array( $this, 'disable_compat_mode' ) );
@@ -736,6 +738,9 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
* default: false
* ---
*
* [--ignore-plugin-compatibility]
* : Enable even if there are active plugins that are incompatible with HPOS.
*
* ### EXAMPLES
*
* # Enable HPOS on new shops.
@@ -750,8 +755,9 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
$assoc_args = wp_parse_args(
$assoc_args,
array(
'for-new-shop' => false,
'with-sync' => false,
'for-new-shop' => false,
'with-sync' => false,
'ignore-plugin-compatibility' => false,
)
);
@@ -763,12 +769,18 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
WP_CLI::error( __( '[Failed] This is not a new shop, but --for-new-shop flag was passed.', 'woocommerce' ) );
}
$container = wc_get_container();
/** Feature controller instance @var FeaturesController $feature_controller */
$feature_controller = wc_get_container()->get( FeaturesController::class );
$plugin_info = $feature_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
if ( count( array_merge( $plugin_info['uncertain'], $plugin_info['incompatible'] ) ) > 0 ) {
WP_CLI::warning( __( '[Failed] Some installed plugins are incompatible. Please review the plugins by going to WooCommerce > Settings > Advanced > Features and see the "Order data storage" section.', 'woocommerce' ) );
$enable_hpos = false;
$feature_controller = $container->get( FeaturesController::class );
if ( ! $assoc_args['ignore-plugin-compatibility'] ) {
$compatibility_info = $feature_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
/** Plugin util instance @var PluginUtil $plugin_util */
$plugin_util = $container->get( PluginUtil::class );
$incompatibles = $plugin_util->get_items_considered_incompatible( 'custom_order_tables', $compatibility_info );
if ( count( $incompatibles ) > 0 ) {
WP_CLI::warning( __( '[Failed] Some installed plugins are incompatible. Please review the plugins by going to WooCommerce > Settings > Advanced > Features and see the "Order data storage" section.', 'woocommerce' ) );
$enable_hpos = false;
}
}
/** DataSynchronizer instance @var DataSynchronizer $data_synchronizer */
@@ -1213,6 +1225,104 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
);
}
/**
* Show the list of WooCommerce-aware plugins known to be compatible, incompatible or without compatibility declaration for HPOS. Note that inactive plugins will always be listed in the "uncertain" list.
*
* [--include-inactive]
* : Include inactive plugins in the list.
*
* [--display-filenames]
* : Print plugin file names instead of plugin names.
*
* @since 9.1.0
*
* @param array $args Positional arguments passed to the command.
* @param array $assoc_args Associative arguments (options) passed to the command.
*/
public function compatibility_info( array $args = array(), array $assoc_args = array() ): void {
$container = wc_get_container();
$feature_controller = $container->get( FeaturesController::class );
$plugin_info = $feature_controller->get_compatible_plugins_for_feature( 'custom_order_tables', ! ( (bool) ( $assoc_args['include-inactive'] ?? null ) ) );
$display_filenames = (bool) ( $assoc_args['display-filenames'] ?? null );
$compatibles = $this->get_printable_plugin_names( $plugin_info['compatible'], $display_filenames );
$compatibles_count = count( $compatibles );
$this->log(
sprintf(
// translators: $1$d = plugins count, %2$s = colon (if list follows) or empty.
_n( "\n%%C%1\$d%%n compatible plugin found%2\$s", "\n%%C%1\$d%%n compatible plugins found%2\$s", $compatibles_count, 'woocommerce' ),
$compatibles_count,
$compatibles_count > 0 ? ":\n" : ''
)
);
$this->print_plugin_names( $compatibles );
$incompatibles = $this->get_printable_plugin_names( $plugin_info['incompatible'], $display_filenames );
$incompatibles_count = count( $incompatibles );
$this->log(
sprintf(
// translators: $1$d = plugins count, %2$s = colon (if list follows) or empty.
_n( "\n%%C%1\$d%%n incompatible plugin found%2\$s", "\n%%C%1\$d%%n incompatible plugins found%2\$s", $incompatibles_count, 'woocommerce' ),
$incompatibles_count,
$incompatibles_count > 0 ? ":\n" : ''
)
);
$this->print_plugin_names( $incompatibles );
$uncertain = $this->get_printable_plugin_names( $plugin_info['uncertain'], $display_filenames );
$uncertain_count = count( $uncertain );
$this->log(
sprintf(
// translators: $1$d = plugins count, %2$s = colon (if list follows) or empty.
_n( "\n%%C%1\$d%%n uncertain plugin found%2\$s", "\n%%C%1\$d%%n uncertain plugins found%2\$s", $uncertain_count, 'woocommerce' ),
$uncertain_count,
$uncertain_count > 0 ? ":\n" : ''
)
);
$this->print_plugin_names( $uncertain );
}
/**
* Get the printable names for a set of plugins given their file names.
*
* @param array $plugins The plugin file names.
* @param bool $display_filenames True to simply return the sorted list of plugin file names.
* @return array A sorted array of plugin names or file names.
*/
private function get_printable_plugin_names( array $plugins, bool $display_filenames ): array {
if ( $display_filenames ) {
sort( $plugins );
return $plugins;
}
$plugin_names = array_map(
fn( $plugin_file ) => get_plugin_data( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin_file, false )['Name'] ?? $plugin_file,
$plugins
);
sort( $plugin_names );
return $plugin_names;
}
/**
* Print a list of plugin names.
*
* @param array $plugins The names to print.
*/
private function print_plugin_names( array $plugins ): void {
foreach ( $plugins as $plugin_file ) {
$this->log( ' ' . $plugin_file );
}
}
/**
* Show a log message using the WP_CLI text colorization feature.
*
* @param string $text Text to show.
*/
private function log( string $text ) {
WP_CLI::log( WP_CLI::colorize( $text ) );
}
/**
* Enables compatibility mode, which keeps the HPOS and posts datastore in sync.
*

View File

@@ -9,7 +9,7 @@ namespace Automattic\WooCommerce\Database\Migrations;
* Base class for implementing migrations from the standard WordPress meta table
* to custom structured tables.
*
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
* @package Automattic\WooCommerce\Database\Migrations
*/
abstract class MetaToCustomTableMigrator extends TableMigrator {

View File

@@ -9,7 +9,7 @@ namespace Automattic\WooCommerce\Database\Migrations;
* Base class for implementing migrations from the standard WordPress meta table
* to custom meta (key-value pairs) tables.
*
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
* @package Automattic\WooCommerce\Database\Migrations
*/
abstract class MetaToMetaTableMigrator extends TableMigrator {

View File

@@ -9,7 +9,7 @@ namespace Automattic\WooCommerce\Database\Migrations;
* Base class for implementing WP posts to order tables migrations handlers.
* It mainly contains methods to deal with error handling.
*
* @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable
* @package Automattic\WooCommerce\Database\Migrations
*/
abstract class TableMigrator {

View File

@@ -23,6 +23,13 @@ class Marketplace {
* @internal
*/
final public function init() {
add_action( 'init', array( $this, 'on_init' ) );
}
/**
* Hook into WordPress on init.
*/
public function on_init() {
if ( false === FeaturesUtil::feature_is_enabled( 'marketplace' ) ) {
/** Feature controller instance @var FeaturesController $feature_controller */
$feature_controller = wc_get_container()->get( FeaturesController::class );

View File

@@ -19,7 +19,7 @@ use Automattic\WooCommerce\Admin\PageController;
class WooSubscriptionsNotes {
const LAST_REFRESH_OPTION_KEY = 'woocommerce_admin-wc-helper-last-refresh';
const NOTE_NAME = 'wc-admin-wc-helper-connection';
const CONNECTION_NOTE_NAME = 'wc-admin-wc-helper-connection';
const CONNECTION_NOTE_NAME = 'wc-admin-wc-helper-connection'; // deprecated.
const SUBSCRIPTION_NOTE_NAME = 'wc-admin-wc-helper-subscription';
const NOTIFY_WHEN_DAYS_LEFT = 60;
@@ -63,7 +63,6 @@ class WooSubscriptionsNotes {
// The site just disconnected.
if ( ! empty( $old_token ) && empty( $new_token ) ) {
$this->remove_notes();
$this->add_no_connection_note();
return;
}
@@ -124,7 +123,6 @@ class WooSubscriptionsNotes {
}
$this->remove_notes();
$this->add_no_connection_note();
}
}
@@ -186,34 +184,6 @@ class WooSubscriptionsNotes {
Notes::delete_notes_with_name( self::SUBSCRIPTION_NOTE_NAME );
}
/**
* Adds a note prompting to connect to WooCommerce.com.
*/
public function add_no_connection_note() {
$note = self::get_note();
$note->save();
}
/**
* Get the WooCommerce.com connection note
*/
public static function get_note() {
$note = new Note();
$note->set_title( __( 'Connect to WooCommerce.com', 'woocommerce' ) );
$note->set_content( __( 'Connect to get important product notifications and updates.', 'woocommerce' ) );
$note->set_content_data( (object) array() );
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
$note->set_name( self::CONNECTION_NOTE_NAME );
$note->set_source( 'woocommerce-admin' );
$note->add_action(
'connect',
__( 'Connect', 'woocommerce' ),
'?page=wc-addons&section=helper',
Note::E_WC_ADMIN_NOTE_UNACTIONED
);
return $note;
}
/**
* Gets the product_id (if any) associated with a note.
*

View File

@@ -976,6 +976,14 @@ class ListTable extends WP_List_Table {
echo '<a href="#" class="order-preview" data-order-id="' . absint( $order->get_id() ) . '" title="' . esc_attr( __( 'Preview', 'woocommerce' ) ) . '">' . esc_html( __( 'Preview', 'woocommerce' ) ) . '</a>';
echo '<a href="' . esc_url( $this->get_order_edit_link( $order ) ) . '" class="order-view"><strong>#' . esc_attr( $order->get_order_number() ) . ' ' . esc_html( $buyer ) . '</strong></a>';
}
// Used for showing date & status next to order number/buyer name on small screens.
echo '<div class="order_date small-screen-only">';
$this->render_order_date_column( $order );
echo '</div>';
echo '<div class="order_status small-screen-only">';
$this->render_order_status_column( $order );
echo '</div>';
}
/**
@@ -1056,14 +1064,14 @@ class ListTable extends WP_List_Table {
*/
private function get_order_status_label( WC_Order $order ): string {
$status_names = array(
'Pending payment' => __( 'The order has been received, but no payment has been made. Pending payment orders are generally awaiting customer action.', 'woocommerce' ),
'On hold' => __( 'The order is awaiting payment confirmation. Stock is reduced, but you need to confirm payment.', 'woocommerce' ),
'Processing' => __( 'Payment has been received (paid), and the stock has been reduced. The order is awaiting fulfillment.', 'woocommerce' ),
'Completed' => __( 'Order fulfilled and complete.', 'woocommerce' ),
'Failed' => __( 'The customers payment failed or was declined, and no payment has been successfully made.', 'woocommerce' ),
'Draft' => __( 'Draft orders are created when customers start the checkout process while the block version of the checkout is in place.', 'woocommerce' ),
'Canceled' => __( 'The order was canceled by an admin or the customer.', 'woocommerce' ),
'Refunded' => __( 'Orders are automatically put in the Refunded status when an admin or shop manager has fully refunded the orders value after payment.', 'woocommerce' ),
'pending' => __( 'The order has been received, but no payment has been made. Pending payment orders are generally awaiting customer action.', 'woocommerce' ),
'on-hold' => __( 'The order is awaiting payment confirmation. Stock is reduced, but you need to confirm payment.', 'woocommerce' ),
'processing' => __( 'Payment has been received (paid), and the stock has been reduced. The order is awaiting fulfillment.', 'woocommerce' ),
'completed' => __( 'Order fulfilled and complete.', 'woocommerce' ),
'failed' => __( 'The customers payment failed or was declined, and no payment has been successfully made.', 'woocommerce' ),
'checkout-draft' => __( 'Draft orders are created when customers start the checkout process while the block version of the checkout is in place.', 'woocommerce' ),
'cancelled' => __( 'The order was canceled by an admin or the customer.', 'woocommerce' ),
'refunded' => __( 'Orders are automatically put in the Refunded status when an admin or shop manager has fully refunded the orders value after payment.', 'woocommerce' ),
);
/**
@@ -1073,9 +1081,9 @@ class ListTable extends WP_List_Table {
* @param WC_Order $order Current order object.
* @since 9.1.0
*/
$status_names = apply_filters( 'woocommerce_get_order_status_labels', $status_names );
$status_names = apply_filters( 'woocommerce_get_order_status_labels', $status_names, $order );
$status_name = wc_get_order_status_name( $order->get_status() );
$status_name = $order->get_status();
return isset( $status_names[ $status_name ] ) ? $status_names[ $status_name ] : '';
}

View File

@@ -93,10 +93,10 @@ class DefaultFreeExtensions {
$plugins = array(
'google-listings-and-ads' => array(
'min_php_version' => '7.4',
'name' => __( 'Google Listings & Ads', 'woocommerce' ),
'name' => __( 'Google for WooCommerce', 'woocommerce' ),
'description' => sprintf(
/* translators: 1: opening product link tag. 2: closing link tag */
__( 'Drive sales with %1$sGoogle Listings and Ads%2$s', 'woocommerce' ),
__( 'Drive sales with %1$sGoogle for WooCommerce%2$s', 'woocommerce' ),
'<a href="https://woocommerce.com/products/google-listings-and-ads" target="_blank">',
'</a>'
),
@@ -116,7 +116,7 @@ class DefaultFreeExtensions {
),
),
'google-listings-and-ads:alt' => array(
'name' => __( 'Google Listings & Ads', 'woocommerce' ),
'name' => __( 'Google for WooCommerce', 'woocommerce' ),
'description' => __( 'Reach more shoppers and drive sales for your store. Integrate with Google to list your products for free and launch paid ad campaigns.', 'woocommerce' ),
'image_url' => plugins_url( '/assets/images/onboarding/google.svg', WC_PLUGIN_FILE ),
'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart',
@@ -413,6 +413,24 @@ class DefaultFreeExtensions {
),
),
),
array(
'type' => 'not',
'operand' => array(
array(
'type' => 'plugins_activated',
'plugins' => array( 'woocommerce-shipping' ),
),
),
),
array(
'type' => 'not',
'operand' => array(
array(
'type' => 'plugins_activated',
'plugins' => array( 'woocommerce-tax' ),
),
),
),
array(
'type' => 'or',
'operands' => array(
@@ -468,63 +486,13 @@ class DefaultFreeExtensions {
'</a>'
),
'is_visible' => array(
self::get_rules_for_wcservices_tax_countries(),
array(
'type' => 'or',
'operands' => array(
'type' => 'not',
'operand' => array(
array(
'type' => 'base_location_country',
'value' => 'US',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'FR',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'GB',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'DE',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'CA',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'AU',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'GR',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'BE',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'PT',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'DK',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'SE',
'operation' => '=',
'type' => 'plugins_activated',
'plugins' => array( 'woocommerce-services' ),
),
),
),
@@ -533,7 +501,16 @@ class DefaultFreeExtensions {
'operand' => array(
array(
'type' => 'plugins_activated',
'plugins' => array( 'woocommerce-services' ),
'plugins' => array( 'woocommerce-shipping' ),
),
),
),
array(
'type' => 'not',
'operand' => array(
array(
'type' => 'plugins_activated',
'plugins' => array( 'woocommerce-tax' ),
),
),
),
@@ -885,7 +862,7 @@ class DefaultFreeExtensions {
'install_priority' => 1,
),
'google-listings-and-ads' => array(
'label' => __( 'Drive sales with Google Listings & Ads', 'woocommerce' ),
'label' => __( 'Drive sales with Google for WooCommerce', 'woocommerce' ),
'image_url' => plugins_url( '/assets/images/core-profiler/logo-google.svg', WC_PLUGIN_FILE ),
'description' => __( 'Reach millions of active shoppers across Google with free product listings and ads.', 'woocommerce' ),
'learn_more_link' => 'https://woocommerce.com/products/google-listings-and-ads?utm_source=storeprofiler&utm_medium=product&utm_campaign=freefeatures',
@@ -900,13 +877,57 @@ class DefaultFreeExtensions {
),
);
// Copy shipping for the core-profiler and remove is_visible conditions, except for the country restriction.
/*
* Overwrite the is_visible conditions to just the country restriction
* and the requirement for WooCommerce Shipping and WooCommerce Tax
* to not be active.
*/
$_plugins['woocommerce-services:shipping']['is_visible'] = array(
array(
'type' => 'base_location_country',
'value' => 'US',
'operation' => '=',
),
array(
'type' => 'not',
'operand' => array(
array(
'type' => 'plugins_activated',
'plugins' => array( 'woocommerce-shipping' ),
),
),
),
array(
'type' => 'not',
'operand' => array(
array(
'type' => 'plugins_activated',
'plugins' => array( 'woocommerce-tax' ),
),
),
),
);
$_plugins['woocommerce-services:tax']['is_visible'] = array(
self::get_rules_for_wcservices_tax_countries(),
array(
'type' => 'not',
'operand' => array(
array(
'type' => 'plugins_activated',
'plugins' => array( 'woocommerce-shipping' ),
),
),
),
array(
'type' => 'not',
'operand' => array(
array(
'type' => 'plugins_activated',
'plugins' => array( 'woocommerce-tax' ),
),
),
),
);
$remove_plugins_activated_rule = function ( $is_visible ) {
@@ -934,7 +955,32 @@ class DefaultFreeExtensions {
foreach ( $plugins as &$plugin ) {
if ( isset( $_plugins[ $plugin['key'] ] ) ) {
$plugin = array_merge( $plugin, $_plugins[ $plugin['key'] ] );
if ( isset( $plugin['is_visible'] ) && is_array( $plugin['is_visible'] ) ) {
/*
* Removes the "not plugins_activated" rules from the "is_visible"
* ruleset except for the WooCommerce Services plugin.
*
* WC Services is a plugin that provides shipping and tax features.
* WC Services is sometimes labelled as "WooCommerce Shipping" or
* "WooCommerce Tax", depending on which functionality of the plugin
* is advertised.
*
* We have two new upcoming, standalone plugins: "WooCommerce Shipping" and
* "WooCommerce Tax" (same names as sometimes used for WC Services).
* The new plugins are incompatible with the old WC Services plugin.
* In order to prevent merchants from running into this plugin conflict,
* we want to keep the "not plugins_activated" rules for recommending
* WC Services.
*
* If WC Services and the new plugins are installed together,
* a notice is displayed and the plugin functionality is not registered
* by either WC Services or WC Shipping and WC Tax.
*/
if (
isset( $plugin['is_visible'] ) &&
is_array( $plugin['is_visible'] ) &&
! in_array( $plugin['key'], array( 'woocommerce-services:shipping', 'woocommerce-services:tax' ), true )
) {
$plugin['is_visible'] = $remove_plugins_activated_rule( $plugin['is_visible'] );
}
}
@@ -942,4 +988,73 @@ class DefaultFreeExtensions {
return $plugins;
}
/**
* Returns the country restrictions for use in the `is_visible` key for
* recommending the tax functionality of WooCommerce Shipping & Tax.
*
* @return array
*/
private static function get_rules_for_wcservices_tax_countries() {
return array(
'type' => 'or',
'operands' => array(
array(
'type' => 'base_location_country',
'value' => 'US',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'FR',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'GB',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'DE',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'CA',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'AU',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'GR',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'BE',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'PT',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'DK',
'operation' => '=',
),
array(
'type' => 'base_location_country',
'value' => 'SE',
'operation' => '=',
),
),
);
}
}

View File

@@ -60,7 +60,9 @@ class ShippingLabelBanner {
$incompatible_plugins = class_exists( '\WC_Shipping_Fedex_Init' ) ||
class_exists( '\WC_Shipping_UPS_Init' ) ||
class_exists( '\WC_Integration_ShippingEasy' ) ||
class_exists( '\WC_ShipStation_Integration' );
class_exists( '\WC_ShipStation_Integration' ) ||
class_exists( '\Automattic\WCShipping\Loader' ) ||
class_exists( '\Automattic\WCTax\Loader' );
$this->shipping_label_banner_display_rules =
new ShippingLabelBannerDisplayRules(

View File

@@ -373,7 +373,7 @@ class WCAdminAssets {
),
array(
'handle' => WC_ADMIN_APP,
'dependencies' => array( 'wc-components', 'wc-admin-layout', 'wc-customer-effort-score', 'wc-product-editor', 'wp-components', 'wc-experimental' ),
'dependencies' => array( 'wc-components', 'wc-admin-layout', 'wc-customer-effort-score', 'wp-components', 'wc-experimental' ),
),
array(
'handle' => 'wc-onboarding',

View File

@@ -114,7 +114,7 @@ class Init extends RemoteSpecsEngine {
$locale = get_user_locale();
$specs = self::get_specs();
$results = EvaluateSuggestion::evaluate_specs( $specs );
$results = EvaluateSuggestion::evaluate_specs( $specs, array( 'source' => 'wc-wcpay-promotions' ) );
if ( count( $results['errors'] ) > 0 ) {
// Unlike payment gateway suggestions, we don't have a non-empty default set of promotions to fall back to.

View File

@@ -24,7 +24,7 @@ namespace Automattic\WooCommerce\Internal\BatchProcessing;
/**
* Class BatchProcessingController
*
* @package Automattic\WooCommerce\Internal\Updates.
* @package Automattic\WooCommerce\Internal\BatchProcessing.
*/
class BatchProcessingController {
/*
@@ -220,17 +220,19 @@ class BatchProcessingController {
* @return array Current state for the processor, or a "blank" state if none exists yet.
*/
private function get_process_details( BatchProcessorInterface $batch_processor ): array {
return get_option(
$this->get_processor_state_option_name( $batch_processor ),
array(
'total_time_spent' => 0,
'current_batch_size' => $batch_processor->get_default_batch_size(),
'last_error' => null,
'recent_failures' => 0,
'batch_first_failure' => null,
'batch_last_failure' => null,
)
$defaults = array(
'total_time_spent' => 0,
'current_batch_size' => $batch_processor->get_default_batch_size(),
'last_error' => null,
'recent_failures' => 0,
'batch_first_failure' => null,
'batch_last_failure' => null,
);
$process_details = get_option( $this->get_processor_state_option_name( $batch_processor ) );
$process_details = wp_parse_args( is_array( $process_details ) ? $process_details : array(), $defaults );
return $process_details;
}
/**
@@ -349,7 +351,15 @@ class BatchProcessingController {
* @return array List (of string) of the class names of the enqueued processors.
*/
public function get_enqueued_processors(): array {
return get_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, array() );
$enqueued_processors = get_option( self::ENQUEUED_PROCESSORS_OPTION_NAME, array() );
if ( ! is_array( $enqueued_processors ) ) {
$this->logger->error( 'Could not fetch list of processors. Clearing up queue.', array( 'source' => 'batch-processing' ) );
delete_option( self::ENQUEUED_PROCESSORS_OPTION_NAME );
$enqueued_processors = array();
}
return $enqueued_processors;
}
/**

View File

@@ -8,7 +8,7 @@ namespace Automattic\WooCommerce\Internal\BatchProcessing;
/**
* Interface BatchProcessorInterface
*
* @package Automattic\WooCommerce\DataBase
* @package Automattic\WooCommerce\Internal\BatchProcessing
*/
interface BatchProcessorInterface {

View File

@@ -2,10 +2,13 @@
namespace Automattic\WooCommerce\Internal\ComingSoon;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Blocks\BlockTemplatesController;
use Automattic\WooCommerce\Blocks\BlockTemplatesRegistry;
use Automattic\WooCommerce\Blocks\Package as BlocksPackage;
/**
* Handles the template_include hook to determine whether the current page needs
* to be replaced with a comiing soon screen.
* to be replaced with a coming soon screen.
*/
class ComingSoonRequestHandler {
@@ -48,13 +51,21 @@ class ComingSoonRequestHandler {
// A coming soon page needs to be displayed. Don't cache this response.
nocache_headers();
$is_fse_theme = wc_current_theme_is_fse_theme();
$is_store_coming_soon = $this->coming_soon_helper->is_store_coming_soon();
if ( ! $is_fse_theme && ! current_theme_supports( 'block-template-parts' ) ) {
// Initialize block templates for use in classic theme.
BlocksPackage::init();
$container = BlocksPackage::container();
$container->get( BlockTemplatesRegistry::class )->init();
$container->get( BlockTemplatesController::class )->init();
}
add_theme_support( 'block-templates' );
$coming_soon_template = get_query_template( 'coming-soon' );
$is_fse_theme = wc_current_theme_is_fse_theme();
$is_store_coming_soon = $this->coming_soon_helper->is_store_coming_soon();
if ( ! $is_fse_theme && $is_store_coming_soon ) {
get_header();
}
@@ -66,7 +77,9 @@ class ComingSoonRequestHandler {
}
);
include $coming_soon_template;
if ( ! empty( $coming_soon_template ) && file_exists( $coming_soon_template ) ) {
include $coming_soon_template;
}
if ( ! $is_fse_theme && $is_store_coming_soon ) {
get_footer();
@@ -185,7 +198,7 @@ class ComingSoonRequestHandler {
foreach ( $fonts_to_add as $font_to_add ) {
$found = false;
foreach ( $font_data as $font ) {
if ( $font['name'] === $font_to_add['name'] ) {
if ( isset( $font['name'] ) && $font['name'] === $font_to_add['name'] ) {
$found = true;
break;
}

View File

@@ -501,12 +501,13 @@ class CustomOrdersTableController {
*/
private function add_feature_definition( $features_controller ) {
$definition = array(
'option_key' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
'is_experimental' => false,
'enabled_by_default' => false,
'order' => 50,
'setting' => $this->get_hpos_setting_for_feature(),
'additional_settings' => array(
'option_key' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
'is_experimental' => false,
'enabled_by_default' => false,
'order' => 50,
'setting' => $this->get_hpos_setting_for_feature(),
'plugins_are_incompatible_by_default' => true,
'additional_settings' => array(
$this->get_hpos_setting_for_sync(),
),
);
@@ -544,11 +545,11 @@ class CustomOrdersTableController {
};
$get_disabled = function () {
$plugin_compatibility = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
$sync_complete = 0 === $this->data_synchronizer->get_current_orders_pending_sync_count();
$disabled = array();
// Changing something here? might also want to look at `enable|disable` functions in CLIRunner.
$incompatible_plugins = array_merge( $plugin_compatibility['uncertain'], $plugin_compatibility['incompatible'] );
$compatibility_info = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
$sync_complete = 0 === $this->data_synchronizer->get_current_orders_pending_sync_count();
$disabled = array();
// Changing something here? You might also want to look at `enable|disable` functions in Automattic\WooCommerce\Database\Migrations\CustomOrderTable\CLIRunner.
$incompatible_plugins = $this->plugin_util->get_items_considered_incompatible( 'custom_order_tables', $compatibility_info );
$incompatible_plugins = array_diff( $incompatible_plugins, $this->plugin_util->get_plugins_excluded_from_compatibility_ui() );
if ( count( $incompatible_plugins ) > 0 ) {
$disabled = array( 'yes' );

View File

@@ -2524,7 +2524,7 @@ FROM $order_meta_table
$this->persist_save( $order );
// Do not fire 'woocommerce_new_order' for draft statuses for backwards compatibility.
if ( 'auto-draft' === $order->get_status( 'edit' ) ) {
if ( in_array( $order->get_status( 'edit' ), array( 'auto-draft', 'draft', 'checkout-draft' ), true ) ) {
return;
}
@@ -2586,8 +2586,7 @@ FROM $order_meta_table
* @param \WC_Order $order Order object.
*/
public function update( &$order ) {
$previous_status = ArrayUtil::get_value_or_default( $order->get_data(), 'status' );
$changes = $order->get_changes();
$previous_status = ArrayUtil::get_value_or_default( $order->get_data(), 'status', 'new' );
// Before updating, ensure date paid is set if missing.
if (
@@ -2622,8 +2621,15 @@ FROM $order_meta_table
$order->apply_changes();
$this->clear_caches( $order );
// For backwards compatibility, moving an auto-draft order to a valid status triggers the 'woocommerce_new_order' hook.
if ( ! empty( $changes['status'] ) && 'auto-draft' === $previous_status ) {
$draft_statuses = array( 'new', 'auto-draft', 'draft', 'checkout-draft' );
// For backwards compatibility, this hook should be fired only if the new status is not one of the draft statuses and the previous status was one of the draft statuses.
if (
! empty( $changes['status'] )
&& $changes['status'] !== $previous_status
&& ! in_array( $changes['status'], $draft_statuses, true )
&& in_array( $previous_status, $draft_statuses, true )
) {
do_action( 'woocommerce_new_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
return;
}

View File

@@ -5,7 +5,7 @@
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\CLIRunner;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;

View File

@@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Internal\Admin\Logging\{ PageController, Settings };
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\FileController;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\Logging\RemoteLogger;
/**
* LoggingServiceProvider class.
@@ -19,6 +20,7 @@ class LoggingServiceProvider extends AbstractServiceProvider {
FileController::class,
PageController::class,
Settings::class,
RemoteLogger::class,
);
/**
@@ -37,5 +39,7 @@ class LoggingServiceProvider extends AbstractServiceProvider {
);
$this->share( Settings::class );
$this->share( RemoteLogger::class );
}
}

View File

@@ -8,7 +8,7 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Caches\OrderCacheController;
use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\CLIRunner;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableRefundDataStore;

View File

@@ -11,6 +11,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider
use Automattic\WooCommerce\Internal\Utilities\COTMigrationUtil;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Internal\Utilities\HtmlSanitizer;
use Automattic\WooCommerce\Internal\Utilities\LegacyRestApiStub;
use Automattic\WooCommerce\Internal\Utilities\PluginInstaller;
use Automattic\WooCommerce\Internal\Utilities\WebhookUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
@@ -39,6 +40,7 @@ class UtilsClassesServiceProvider extends AbstractInterfaceServiceProvider {
RestApiUtil::class,
TimeUtil::class,
PluginInstaller::class,
LegacyRestApiStub::class,
);
/**
@@ -56,5 +58,6 @@ class UtilsClassesServiceProvider extends AbstractInterfaceServiceProvider {
$this->share( RestApiUtil::class );
$this->share( TimeUtil::class );
$this->share_with_implements_tags( PluginInstaller::class );
$this->share_with_implements_tags( LegacyRestApiStub::class )->addArgument( RestApiUtil::class );
}
}

View File

@@ -26,6 +26,8 @@ class FeaturesController {
public const FEATURE_ENABLED_CHANGED_ACTION = 'woocommerce_feature_enabled_changed';
public const PLUGINS_COMPATIBLE_BY_DEFAULT_OPTION = 'woocommerce_plugins_are_compatible_with_features_by_default';
/**
* The existing feature definitions.
*
@@ -142,12 +144,13 @@ class FeaturesController {
*/
public function add_feature_definition( $slug, $name, array $args = array() ) {
$defaults = array(
'disable_ui' => false,
'enabled_by_default' => false,
'is_experimental' => true,
'is_legacy' => false,
'name' => $name,
'order' => 10,
'disable_ui' => false,
'enabled_by_default' => false,
'is_experimental' => true,
'is_legacy' => false,
'plugins_are_incompatible_by_default' => false,
'name' => $name,
'order' => 10,
);
$args = wp_parse_args( $args, $defaults );
@@ -244,6 +247,17 @@ class FeaturesController {
'is_legacy' => true,
'option_key' => CustomOrdersTableController::HPOS_FTS_INDEX_OPTION,
),
'remote_logging' => array(
'name' => __( 'Remote Logging', 'woocommerce' ),
'description' => __(
'Enable this feature to log errors and related data to Automattic servers for debugging purposes and to improve WooCommerce',
'woocommerce'
),
'enabled_by_default' => false,
'disable_ui' => true,
'is_legacy' => false,
'is_experimental' => true,
),
);
foreach ( $legacy_features as $slug => $definition ) {
@@ -322,6 +336,34 @@ class FeaturesController {
return $features;
}
/**
* Check if plugins that don't declare compatibility nor incompatibility with a given feature
* are to be considered incompatible with that feature.
*
* @param string $feature_id Feature id to check.
* @return bool True if plugins that don't declare compatibility nor incompatibility with the feature will be considered incompatible with the feature.
* @throws \InvalidArgumentException The feature doesn't exist.
*/
public function get_plugins_are_incompatible_by_default( string $feature_id ): bool {
$feature_definition = $this->get_feature_definitions()[ $feature_id ] ?? null;
if ( is_null( $feature_definition ) ) {
throw new \InvalidArgumentException( esc_html( "The WooCommerce feature '$feature_id' doesn't exist" ) );
}
$incompatible_by_default = $feature_definition['plugins_are_incompatible_by_default'] ?? false;
/**
* Filter to determine if plugins that don't declare compatibility nor incompatibility with a given feature
* are to be considered incompatible with that feature.
*
* @param bool $incompatible_by_default Default value, true if plugins are to be considered incompatible by default with the feature.
* @param string $feature_id The feature to check.
*
* @since 9.2.0
*/
return (bool) apply_filters( 'woocommerce_plugins_are_incompatible_with_feature_by_default', $incompatible_by_default, $feature_id );
}
/**
* Check if a given feature is currently enabled.
*
@@ -476,7 +518,7 @@ class FeaturesController {
*
* @param string $feature_id Feature id.
* @param bool $active_only True to return only active plugins.
* @return array An array having a 'compatible' and an 'incompatible' key, each holding an array of plugin names.
* @return array An array having a 'compatible', an 'incompatible' and an 'uncertain' key, each holding an array of plugin names.
*/
public function get_compatible_plugins_for_feature( string $feature_id, bool $active_only = false ): array {
$this->verify_did_woocommerce_init( __FUNCTION__ );
@@ -884,7 +926,7 @@ class FeaturesController {
*
* @param array $plugin_list The original list of plugins.
*/
private function filter_plugins_list( $plugin_list ): array {
public function filter_plugins_list( $plugin_list ): array {
if ( ! $this->verify_did_woocommerce_init() ) {
return $plugin_list;
}
@@ -910,10 +952,11 @@ class FeaturesController {
*
* @return array List of plugins incompatible with the given feature.
*/
private function get_incompatible_plugins( $feature_id, $plugin_list ) {
$incompatibles = array();
$plugin_list = array_diff_key( $plugin_list, array_flip( $this->plugins_excluded_from_compatibility_ui ) );
public function get_incompatible_plugins( $feature_id, $plugin_list ) {
$incompatibles = array();
$plugin_list = array_diff_key( $plugin_list, array_flip( $this->plugins_excluded_from_compatibility_ui ) );
$feature_ids = 'all' === $feature_id ? array_keys( $this->get_feature_definitions() ) : array( $feature_id );
$only_enabled_features = 'all' === $feature_id;
// phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
foreach ( array_keys( $plugin_list ) as $plugin_name ) {
@@ -921,16 +964,17 @@ class FeaturesController {
continue;
}
$compatibility = $this->get_compatible_features_for_plugin( $plugin_name );
$incompatible_with = array_filter(
array_merge( $compatibility['incompatible'], $compatibility['uncertain'] ),
function ( $feature_id ) {
return ! $this->is_legacy_feature( $feature_id );
$compatibility_info = $this->get_compatible_features_for_plugin( $plugin_name );
foreach ( $feature_ids as $feature_id ) {
$features_considered_incompatible = array_filter(
$this->plugin_util->get_items_considered_incompatible( $feature_id, $compatibility_info ),
$only_enabled_features ?
fn( $feature_id ) => $this->feature_is_enabled( $feature_id ) && ! $this->is_legacy_feature( $feature_id ) :
fn( $feature_id ) => ! $this->is_legacy_feature( $feature_id )
);
if ( in_array( $feature_id, $features_considered_incompatible, true ) ) {
$incompatibles[] = $plugin_name;
}
);
if ( ( 'all' === $feature_id && ! empty( $incompatible_with ) ) || in_array( $feature_id, $incompatible_with, true ) ) {
$incompatibles[] = $plugin_name;
}
}
@@ -954,7 +998,7 @@ class FeaturesController {
/**
* Shows a warning when there are any incompatibility between active plugins and enabled features.
* The warning is shown in on any admin screen except the plugins screen itself, since
* there's already a "You are viewing
* there's already a "You are viewing plugins that are incompatible" notice.
*/
private function maybe_display_feature_incompatibility_warning(): void {
if ( ! current_user_can( 'activate_plugins' ) ) {
@@ -965,18 +1009,25 @@ class FeaturesController {
$relevant_plugins = array_diff( $this->plugin_util->get_woocommerce_aware_plugins( true ), $this->plugins_excluded_from_compatibility_ui );
foreach ( $relevant_plugins as $plugin ) {
$compatibility = $this->get_compatible_features_for_plugin( $plugin, true );
$incompatible_with = array_filter(
array_merge( $compatibility['incompatible'], $compatibility['uncertain'] ),
function ( $feature_id ) {
return ! $this->is_legacy_feature( $feature_id );
}
);
$compatibility_info = $this->get_compatible_features_for_plugin( $plugin, true );
if ( $incompatible_with ) {
$incompatibles = array_filter( $compatibility_info['incompatible'], fn( $feature_id ) => ! $this->is_legacy_feature( $feature_id ) );
if ( ! empty( $incompatibles ) ) {
$incompatible_plugins = true;
break;
}
$uncertains = array_filter( $compatibility_info['uncertain'], fn( $feature_id ) => ! $this->is_legacy_feature( $feature_id ) );
foreach ( $uncertains as $feature_id ) {
if ( $this->get_plugins_are_incompatible_by_default( $feature_id ) ) {
$incompatible_plugins = true;
break;
}
}
if ( $incompatible_plugins ) {
break;
}
}
if ( ! $incompatible_plugins ) {

View File

@@ -309,13 +309,45 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
'order' => 10,
)
);
$product_inventory_inner_section->add_block(
$inventory_columns = $product_inventory_inner_section->add_block(
array(
'id' => 'product-inventory-inner-columns',
'blockName' => 'core/columns',
)
);
$inventory_columns->add_block(
array(
'id' => 'product-inventory-inner-column1',
'blockName' => 'core/column',
)
)->add_block(
array(
'id' => 'product-variation-sku-field',
'blockName' => 'woocommerce/product-sku-field',
'order' => 10,
)
);
$inventory_columns->add_block(
array(
'id' => 'product-inventory-inner-column2',
'blockName' => 'core/column',
)
)->add_block(
array(
'id' => 'product-unique-id-field',
'blockName' => 'woocommerce/product-text-field',
'order' => 20,
'attributes' => array(
'property' => 'global_unique_id',
'label' => __( 'GTIN, UPC, EAN or ISBN', 'woocommerce' ),
'tooltip' => __( 'Enter a barcode or any other identifier unique to this product. It can help you list this product on other channels or marketplaces.', 'woocommerce' ),
'pattern' => array(
'value' => '[0-9\-]*',
'message' => __( 'Please enter only numbers and hyphens (-).', 'woocommerce' ),
),
),
)
);
$product_inventory_inner_section->add_block(
array(
'id' => 'product-variation-track-stock',

View File

@@ -730,7 +730,18 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
'order' => 10,
)
);
$product_inventory_inner_section->add_block(
$inventory_columns = $product_inventory_inner_section->add_block(
array(
'id' => 'product-inventory-inner-columns',
'blockName' => 'core/columns',
)
);
$inventory_columns->add_block(
array(
'id' => 'product-inventory-inner-column1',
'blockName' => 'core/column',
)
)->add_block(
array(
'id' => 'product-sku-field',
'blockName' => 'woocommerce/product-sku-field',
@@ -742,6 +753,32 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
),
)
);
$inventory_columns->add_block(
array(
'id' => 'product-inventory-inner-column2',
'blockName' => 'core/column',
)
)->add_block(
array(
'id' => 'product-unique-id-field',
'blockName' => 'woocommerce/product-text-field',
'order' => 20,
'attributes' => array(
'property' => 'global_unique_id',
'label' => __( 'GTIN, UPC, EAN or ISBN', 'woocommerce' ),
'tooltip' => __( 'Enter a barcode or any other identifier unique to this product. It can help you list this product on other channels or marketplaces.', 'woocommerce' ),
'pattern' => array(
'value' => '[0-9\-]*',
'message' => __( 'Please enter only numbers and hyphens (-).', 'woocommerce' ),
),
),
'disableConditions' => array(
array(
'expression' => 'editedProduct.type === "variable"',
),
),
)
);
$manage_stock = 'yes' === get_option( 'woocommerce_manage_stock' );
$product_inventory_inner_section->add_block(

View File

@@ -31,19 +31,20 @@ class WPConsentAPI {
*/
public function register() {
add_action(
'plugins_loaded',
'init',
function() {
$this->on_plugins_loaded();
}
$this->on_init();
},
20 // After OrderAttributionController.
);
}
/**
* Register our hooks on plugins_loaded.
* Register our hooks on init.
*
* @return void
*/
protected function on_plugins_loaded() {
protected function on_init() {
// Include integration to WP Consent Level API if available.
if ( ! $this->is_wp_consent_api_active() ) {
return;
@@ -61,7 +62,7 @@ class WPConsentAPI {
/**
* Modify the "allowTracking" flag consent if the user has consented to marketing.
*
* Wp-consent-api will initialize the modules on "plugins_loaded" with priority 9,
* Wp-consent-api will initialize the modules on "init" with priority 9,
* So this code needs to be run after that.
*/
add_filter(

View File

@@ -0,0 +1,128 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Internal\Logging;
use Automattic\WooCommerce\Utilities\FeaturesUtil;
/**
* WooCommerce Remote Logger
*
* The WooCommerce remote logger class adds functionality to log WooCommerce errors remotely based on if the customer opted in and several other conditions.
*
* No personal information is logged, only error information and relevant context.
*
* @class RemoteLogger
* @since 9.2.0
* @package WooCommerce\Classes
*/
class RemoteLogger {
const WC_LATEST_VERSION_TRANSIENT = 'latest_woocommerce_version';
const FETCH_LATEST_VERSION_RETRY = 'fetch_latest_woocommerce_version_retry';
/**
* Determines if remote logging is allowed based on the following conditions:
*
* 1. The feature flag for remote error logging is enabled.
* 2. The user has opted into tracking/logging.
* 3. The store is allowed to log based on the variant assignment percentage.
* 4. The current WooCommerce version is the latest so we don't log errors that might have been fixed in a newer version.
*
* @return bool
*/
public function is_remote_logging_allowed() {
if ( ! FeaturesUtil::feature_is_enabled( 'remote_logging' ) ) {
return false;
}
if ( ! $this->is_tracking_opted_in() ) {
return false;
}
if ( ! $this->is_variant_assignment_allowed() ) {
return false;
}
if ( ! $this->is_latest_woocommerce_version() ) {
return false;
}
return true;
}
/**
* Check if the user has opted into tracking/logging.
*
* @return bool
*/
private function is_tracking_opted_in() {
return 'yes' === get_option( 'woocommerce_allow_tracking', 'no' );
}
/**
* Check if the store is allowed to log based on the variant assignment percentage.
*
* @return bool
*/
private function is_variant_assignment_allowed() {
$assignment = get_option( 'woocommerce_remote_variant_assignment', 0 );
return ( $assignment <= 12 ); // Considering 10% of the 0-120 range.
}
/**
* Check if the current WooCommerce version is the latest.
*
* @return bool
*/
private function is_latest_woocommerce_version() {
$latest_wc_version = $this->fetch_latest_woocommerce_version();
if ( is_null( $latest_wc_version ) ) {
return false;
}
return version_compare( WC()->version, $latest_wc_version, '>=' );
}
/**
* Fetch the latest WooCommerce version using the WordPress API and cache it.
*
* @return string|null
*/
private function fetch_latest_woocommerce_version() {
$cached_version = get_transient( self::WC_LATEST_VERSION_TRANSIENT );
if ( $cached_version ) {
return $cached_version;
}
$retry_count = get_transient( self::FETCH_LATEST_VERSION_RETRY );
if ( false === $retry_count || ! is_numeric( $retry_count ) ) {
$retry_count = 0;
}
if ( $retry_count >= 3 ) {
return null;
}
if ( ! function_exists( 'plugins_api' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
}
// Fetch the latest version from the WordPress API.
$plugin_info = plugins_api( 'plugin_information', array( 'slug' => 'woocommerce' ) );
if ( is_wp_error( $plugin_info ) ) {
++$retry_count;
set_transient( self::FETCH_LATEST_VERSION_RETRY, $retry_count, HOUR_IN_SECONDS );
return null;
}
if ( ! empty( $plugin_info->version ) ) {
$latest_version = $plugin_info->version;
set_transient( self::WC_LATEST_VERSION_TRANSIENT, $latest_version, WEEK_IN_SECONDS );
delete_transient( self::FETCH_LATEST_VERSION_RETRY );
return $latest_version;
}
return null;
}
}

View File

@@ -61,11 +61,18 @@ class OrderAttributionBlocksController implements RegisterHooksInterface {
}
/**
* Hook into WP.
* Register this class instance to the appropriate hooks.
*
* @return void
*/
public function register() {
add_action( 'init', array( $this, 'on_init' ) );
}
/**
* Hook into WordPress on init.
*/
public function on_init() {
// Bail if the feature is not enabled.
if ( ! $this->features_controller->feature_is_enabled( 'order_attribution' ) ) {
return;

View File

@@ -95,6 +95,13 @@ class OrderAttributionController implements RegisterHooksInterface {
return;
}
add_action( 'init', array( $this, 'on_init' ) );
}
/**
* Hook into WordPress on init.
*/
public function on_init() {
// Bail if the feature is not enabled.
if ( ! $this->feature_controller->feature_is_enabled( 'order_attribution' ) ) {
return;

View File

@@ -431,9 +431,14 @@ class LookupDataStore {
* Create all the necessary lookup data for a given variation.
*
* @param \WC_Product_Variation $variation The variation to create entries for.
* @throws \Exception Can't retrieve the details of the parent product.
*/
private function create_data_for_variation( \WC_Product_Variation $variation ) {
$main_product = WC()->call_function( 'wc_get_product', $variation->get_parent_id() );
if ( false === $main_product ) {
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
throw new \Exception( "The product is a variation, and the retrieval of data for the parent product (id {$variation->get_parent_id()}) failed." );
}
$product_attributes_data = $this->get_attribute_taxonomies( $main_product );
$variation_attributes_data = array_filter(

View File

@@ -198,8 +198,34 @@ class ReceiptRenderingEngine {
*/
$data['css'] = apply_filters( 'woocommerce_printable_order_receipt_css', $css, $order );
$default_template_path = __DIR__ . '/Templates/order-receipt.php';
/**
* Filter the order receipt template path.
*
* @since 9.2.0
* @hook wc_get_template
* @param string $template The template path.
* @param string $template_name The template name.
* @param array $args The available data for the template.
* @param string $template_path The template path.
* @param string $default_path The default template path.
*/
$template_path = apply_filters(
'wc_get_template',
$default_template_path,
'ReceiptRendering/order-receipt.php',
$data,
$default_template_path,
$default_template_path
);
if ( ! file_exists( $template_path ) ) {
$template_path = $default_template_path;
}
ob_start();
include __DIR__ . '/Templates/order-receipt.php';
include $template_path;
$rendered_template = ob_get_contents();
ob_end_clean();

View File

@@ -6,8 +6,8 @@ use Automattic\WooCommerce\Internal\TransientFiles\TransientFilesEngine;
use \WP_REST_Server;
use \WP_REST_Request;
use \WP_Error;
use \InvalidArgumentException;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Internal\RestApiControllerBase;
/**
* Controller for the REST endpoints associated to the receipt rendering engine.

View File

@@ -1,6 +1,6 @@
<?php
namespace Automattic\WooCommerce\Internal\ReceiptRendering;
namespace Automattic\WooCommerce\Internal;
use Automattic\WooCommerce\Internal\RegisterHooksInterface;
use Automattic\WooCommerce\Utilities\StringUtil;
@@ -88,33 +88,6 @@ abstract class RestApiControllerBase implements RegisterHooksInterface {
*/
protected string $route_namespace = 'wc/v3';
/**
* Holds authentication error messages for each HTTP verb.
*
* @var array
*/
protected array $authentication_errors_by_method;
/**
* Class constructor.
*/
public function __construct() {
$this->authentication_errors_by_method = array(
'GET' => array(
'code' => 'woocommerce_rest_cannot_view',
'message' => __( 'Sorry, you cannot view resources.', 'woocommerce' ),
),
'POST' => array(
'code' => 'woocommerce_rest_cannot_create',
'message' => __( 'Sorry, you cannot create resources.', 'woocommerce' ),
),
'DELETE' => array(
'code' => 'woocommerce_rest_cannot_delete',
'message' => __( 'Sorry, you cannot delete resources.', 'woocommerce' ),
),
);
}
/**
* Register the hooks used by the class.
*/
@@ -193,6 +166,31 @@ abstract class RestApiControllerBase implements RegisterHooksInterface {
return new WP_Error( 'woocommerce_rest_internal_error', __( 'Internal server error', 'woocommerce' ), $data );
}
/**
* Returns an authentication error message for a given HTTP verb.
*
* @param string $method HTTP method.
* @return array|null Error information on success, null otherwise.
*/
protected function get_authentication_error_by_method( string $method ) {
$errors = array(
'GET' => array(
'code' => 'woocommerce_rest_cannot_view',
'message' => __( 'Sorry, you cannot view resources.', 'woocommerce' ),
),
'POST' => array(
'code' => 'woocommerce_rest_cannot_create',
'message' => __( 'Sorry, you cannot create resources.', 'woocommerce' ),
),
'DELETE' => array(
'code' => 'woocommerce_rest_cannot_delete',
'message' => __( 'Sorry, you cannot delete resources.', 'woocommerce' ),
),
);
return $errors[ $method ] ?? null;
}
/**
* Permission check for REST API endpoints, given the request method.
*
@@ -206,7 +204,7 @@ abstract class RestApiControllerBase implements RegisterHooksInterface {
return true;
}
$error_information = $this->authentication_errors_by_method[ $request->get_method() ] ?? null;
$error_information = $this->get_authentication_error_by_method( $request->get_method() );
if ( is_null( $error_information ) ) {
return false;
}

View File

@@ -2,26 +2,52 @@
namespace Automattic\WooCommerce\Internal\Utilities;
use Automattic\WooCommerce\Internal\RegisterHooksInterface;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Utilities\RestApiUtil;
/**
* The Legacy REST API was removed in WooCommerce 9.0 and is now available as a dedicated extension.
* A stub is kept in WooCommerce core that just returns a "The WooCommerce API is disabled on this site"
* error if the extension is not installed or is inactive. See:
* https://developer.woocommerce.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/
* A stub is kept in WooCommerce core that acts when the extension is not installed and has two purposes:
*
* 1. Return a "The WooCommerce API is disabled on this site" error for any request to the Legacy REST API endpoints.
*
* 2. Provide the not-endpoint related utility methods that were previously supplied by the WC_API class,
* this is achieved by setting the value of WooCommerce::api (typically accessed via 'WC()->api') to an instance of this class.
*
* DO NOT add any additional public method to this class unless the method existed with the same signature in the old WC_API class.
*
* See: https://developer.woocommerce.com/2023/10/03/the-legacy-rest-api-will-move-to-a-dedicated-extension-in-woocommerce-9-0/
*/
class LegacyRestApiStub {
class LegacyRestApiStub implements RegisterHooksInterface {
use AccessiblePrivateMethods;
/**
* Set up the Legacy REST API stub.
* The instance of RestApiUtil to use.
*
* @var RestApiUtil
*/
public static function setup() {
private RestApiUtil $rest_api_util;
/**
* Set up the Legacy REST API endpoints stub.
*/
public function register() {
self::add_action( 'init', array( __CLASS__, 'add_rewrite_rules_for_legacy_rest_api_stub' ), 0 );
self::add_action( 'query_vars', array( __CLASS__, 'add_query_vars_for_legacy_rest_api_stub' ), 0 );
self::add_action( 'parse_request', array( __CLASS__, 'parse_legacy_rest_api_request' ), 0 );
}
/**
* Initialize the class dependencies.
*
* @internal
* @param RestApiUtil $rest_api_util The instance of RestApiUtil to use.
*/
final public function init( RestApiUtil $rest_api_util ) {
$this->rest_api_util = $rest_api_util;
}
/**
* Add the necessary rewrite rules for the Legacy REST API
* (either the dedicated extension if it's installed, or the stub otherwise).
@@ -142,4 +168,24 @@ class LegacyRestApiStub {
die( '-1' );
}
}
/**
* Get data from a WooCommerce API endpoint.
* This method used to be part of the WooCommerce Legacy REST API.
*
* @since 9.1.0
*
* @param string $endpoint Endpoint.
* @param array $params Params to pass with request.
* @return array|\WP_Error
*/
public function get_endpoint_data( $endpoint, $params = array() ) {
wc_doing_it_wrong(
'get_endpoint_data',
"'WC()->api->get_endpoint_data' is deprecated, please use the following instead: wc_get_container()->get(Automattic\WooCommerce\Utilities\RestApiUtil::class)->get_endpoint_data",
'9.1.0'
);
return $this->rest_api_util->get_endpoint_data( $endpoint, $params );
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Internal\Utilities;
use InvalidArgumentException;
/**
* Utilities to help ensure type safety.
*/
class Types {
/**
* Checks if $thing is an instance of $desired_type.
*
* If the check succeeds, $thing will be returned without further modification. If the check fails, then either
* an exception will be thrown or, if an $on_failure callback was supplied, it will be invoked to either generate
* an appropriate return value or to throw a more specific exception.
*
* Please note that the failure handler will be passed two arguments:
*
* $on_failure( $object, $desired_type )
*
* @since 9.1.0
* @throws InvalidArgumentException If $object does not match $desired_type, and an $on_failure callback was not supplied.
*
* @param mixed $thing The value or reference to be assessed.
* @param string $desired_type What we expect the return type to be, if it is not a WP_Error.
* @param ?callable $on_failure If provided, and if evaluation fails, this will be invoked to generate a return value.
*
* @return mixed
*/
public static function ensure_instance_of( $thing, string $desired_type, callable $on_failure = null ) {
// If everything looks good, return early.
if ( $thing instanceof $desired_type ) {
return $thing;
}
// Summarize the error for use in logging and in case we have to throw an exception.
$summary = sprintf(
'Object was not of expected type %1$s.',
$desired_type
);
// Otherwise, let's log the problem so the site operator has a record of where things went wrong.
$logger = wc_get_logger();
if ( $logger ) {
$logger->error(
$summary,
array(
'source' => 'wc-type-check-utility',
'backtrace' => true,
)
);
}
// Invoke the $on_failure handler, if specified.
if ( null !== $on_failure ) {
return $on_failure( $thing, $desired_type );
}
throw new InvalidArgumentException( esc_html( $summary ) );
}
}

View File

@@ -8,13 +8,24 @@ namespace Automattic\WooCommerce\StoreApi\Formatters;
*/
class MoneyFormatter implements FormatterInterface {
/**
* Format a given value and return the result.
* Format a given price value and return the result as a string without decimals.
*
* @param mixed $value Value to format.
* @param array $options Options that influence the formatting.
* @return mixed
* @param int|float|string $value Value to format. Int is allowed, as it may also represent a valid price.
* @param array $options Options that influence the formatting.
* @return string
*/
public function format( $value, array $options = [] ) {
if ( ! is_int( $value ) && ! is_string( $value ) && ! is_float( $value ) ) {
wc_doing_it_wrong(
__FUNCTION__,
'Function expects a $value arg of type INT, STRING or FLOAT.',
'9.2'
);
return '';
}
$options = wp_parse_args(
$options,
[
@@ -23,12 +34,20 @@ class MoneyFormatter implements FormatterInterface {
]
);
return (string) intval(
round(
( (float) wc_format_decimal( $value ) ) * ( 10 ** absint( $options['decimals'] ) ),
0,
absint( $options['rounding_mode'] )
)
);
// Ensure rounding mode is valid.
$rounding_modes = [ PHP_ROUND_HALF_UP, PHP_ROUND_HALF_DOWN, PHP_ROUND_HALF_EVEN, PHP_ROUND_HALF_ODD ];
$options['rounding_mode'] = absint( $options['rounding_mode'] );
if ( ! in_array( $options['rounding_mode'], $rounding_modes, true ) ) {
$options['rounding_mode'] = PHP_ROUND_HALF_UP;
}
$value = floatval( $value );
// Remove the price decimal points for rounding purposes.
$value = $value * pow( 10, absint( $options['decimals'] ) );
$value = round( $value, 0, $options['rounding_mode'] );
// This ensures returning the value as a string without decimal points ready for price parsing.
return wc_format_decimal( $value, 0, true );
}
}

View File

@@ -60,8 +60,11 @@ class Legacy {
// and a generic notice will be shown instead if payment failed.
wc_clear_notices();
// Handle result.
$result->set_status( isset( $gateway_result['result'] ) && 'success' === $gateway_result['result'] ? 'success' : 'failure' );
// Handle result. If status was not returned we consider this invalid and return failure.
$result_status = $gateway_result['result'] ?? 'failure';
// These are the same statuses supported by the API and indicate processing status. This is not the same as order status.
$valid_status = array( 'success', 'failure', 'pending', 'error' );
$result->set_status( in_array( $result_status, $valid_status, true ) ? $result_status : 'failure' );
// set payment_details from result.
$result->set_payment_details( array_merge( $result->payment_details, $gateway_result ) );

View File

@@ -105,7 +105,6 @@ abstract class AbstractCartRoute extends AbstractRoute {
*/
public function get_response( \WP_REST_Request $request ) {
$this->load_cart_session( $request );
$this->cart_controller->calculate_totals();
$response = null;
$nonce_check = $this->requires_nonce( $request ) ? $this->check_nonce( $request ) : null;
@@ -332,13 +331,11 @@ abstract class AbstractCartRoute extends AbstractRoute {
* @return \WP_Error WP Error object.
*/
protected function get_route_error_response( $error_code, $error_message, $http_status_code = 500, $additional_data = [] ) {
$additional_data['status'] = $http_status_code;
// If there was a conflict, return the cart so the client can resolve it.
if ( 409 === $http_status_code ) {
$cart = $this->cart_controller->get_cart_instance();
$additional_data['cart'] = $this->cart_schema->get_item_response( $cart );
$additional_data['cart'] = $this->cart_schema->get_item_response( $this->cart_controller->get_cart_for_response() );
}
return new \WP_Error( $error_code, $error_message, $additional_data );

View File

@@ -6,8 +6,6 @@ use Automattic\WooCommerce\StoreApi\Routes\RouteInterface;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\Exceptions\InvalidCartException;
use Automattic\WooCommerce\StoreApi\Schemas\v1\AbstractSchema;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
use Automattic\WooCommerce\Blocks\Package;
use WP_Error;
/**

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