there are feature-plugin incompatibility warnings.
*
* WordPress renders the plugin information rows in the plugins page in
elements as follows:
*
* - If the plugin needs update, the
will have an "update" class. This will prevent the lower
* border line to be drawn. Later an additional
with an "update available" warning will be rendered,
* it will have a "plugin-update-tr" class which will draw the missing lower border line.
* - Otherwise, the
will be already drawn with the lower border line.
*
* This is a problem for our rendering of the "plugin is incompatible with X features" warning:
*
* - If the plugin info
has "update", our
will render nicely right after it; but then
* our own "plugin-update-tr" class will draw an additional line before the "needs update" warning.
* - If not, the plugin info
will render its lower border line right before our compatibility info
.
*
* This small script fixes this by adding the "update" class to the plugin info
if it doesn't have it
* (so no extra line before our
), or removing 'plugin-update-tr' from our
otherwise
* (and then some extra manual tweaking of margins is needed).
*
* @param string $current_screen The current screen object.
*
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
*/
public function enqueue_script_to_fix_plugin_list_html( $current_screen ): void {
if ( 'plugins' !== $current_screen->id ) {
return;
}
$handle = 'wc-features-fix-plugin-list-html';
wp_register_script( $handle, '', array(), WC_VERSION, array( 'in_footer' => true ) );
wp_enqueue_script( $handle );
wp_add_inline_script(
$handle,
"
const warningRows = document.querySelectorAll('tr[data-plugin-row-type=\"feature-incomp-warn\"]');
for(const warningRow of warningRows) {
const pluginName = warningRow.getAttribute('data-plugin');
const pluginInfoRow = document.querySelector('tr.active[data-plugin=\"' + pluginName + '\"]:not(.plugin-update-tr), tr.inactive[data-plugin=\"' + pluginName + '\"]:not(.plugin-update-tr)');
if(!pluginInfoRow) {
continue;
}
if(pluginInfoRow.classList.contains('update')) {
warningRow.classList.remove('plugin-update-tr');
warningRow.querySelector('.notice').style.margin = '5px 10px 15px 30px';
}
else {
pluginInfoRow.classList.add('update');
}
}
"
);
}
/**
* Handler for the 'views_plugins' hook that shows the links to the different views in the plugins page.
* If we come from a "Manage incompatible plugins" in the features page we'll show just two views:
* "All" (so that it's easy to go back to a known state) and "Incompatible with X".
* We'll skip the rest of the views since the counts are wrong anyway, as we are modifying
* the plugins list via the 'all_plugins' filter.
*
* @param array $views An array of view ids => view links.
* @return string[] The actual views array to use.
*
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
*/
public function handle_plugins_page_views_list( $views ): array {
// phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
if ( 'incompatible_with_feature' !== ArrayUtil::get_value_or_default( $_GET, 'plugin_status' ) ) {
return $views;
}
$feature_id = $_GET['feature_id'] ?? 'all';
if ( 'all' !== $feature_id && ! $this->feature_exists( $feature_id ) ) {
return $views;
}
// phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
$all_items = get_plugins();
$features = $this->get_feature_definitions();
$incompatible_plugins_count = count( $this->filter_plugins_list( $all_items ) );
$incompatible_text =
'all' === $feature_id
? __( 'Incompatible with WooCommerce features', 'woocommerce' )
/* translators: %s = name of a WooCommerce feature */
: sprintf( __( "Incompatible with '%s'", 'woocommerce' ), $features[ $feature_id ]['name'] );
$incompatible_link = "{$incompatible_text} ({$incompatible_plugins_count})";
$all_plugins_count = count( $all_items );
$all_text = __( 'All', 'woocommerce' );
$all_link = "{$all_text} ({$all_plugins_count})";
return array(
'all' => $all_link,
'incompatible_with_feature' => $incompatible_link,
);
}
/**
* Set the feature nonce to be sent from client side.
*
* @param array $settings Component settings.
*
* @return array
*
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
*/
public function set_change_feature_enable_nonce( $settings ) {
$settings['_feature_nonce'] = wp_create_nonce( 'change_feature_enable' );
return $settings;
}
/**
* Changes the feature given it's id, a toggle value and nonce as a query param.
*
* `/wp-admin/post.php?feature_id=1&_feature_nonce=1234`, 1 for on
* `/wp-admin/post.php?feature_id=0&_feature_nonce=1234`, 0 for off
*
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
*/
public function change_feature_enable_from_query_params(): void {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return;
}
$is_feature_nonce_invalid = ( ! isset( $_GET['_feature_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_feature_nonce'] ) ), 'change_feature_enable' ) );
$query_params_to_remove = array( '_feature_nonce' );
foreach ( array_keys( $this->get_feature_definitions() ) as $feature_id ) {
if ( isset( $_GET[ $feature_id ] ) && is_numeric( $_GET[ $feature_id ] ) ) {
$value = absint( $_GET[ $feature_id ] );
if ( $is_feature_nonce_invalid ) {
wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce' ) );
return;
}
if ( 1 === $value ) {
$this->change_feature_enable( $feature_id, true );
} elseif ( 0 === $value ) {
$this->change_feature_enable( $feature_id, false );
}
$query_params_to_remove[] = $feature_id;
}
}
if ( count( $query_params_to_remove ) > 1 && isset( $_SERVER['REQUEST_URI'] ) ) {
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
wp_safe_redirect( remove_query_arg( $query_params_to_remove, $_SERVER['REQUEST_URI'] ) );
}
}
/**
* Display the email improvements feedback notice to render CES modal in.
*
* @param string $feature_id The feature id.
* @param bool $is_enabled Whether the feature is enabled.
*
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
*/
public function display_email_improvements_feedback_notice( $feature_id, $is_enabled ): void {
if ( 'email_improvements' === $feature_id && ! $is_enabled ) {
set_transient( 'wc_settings_email_improvements_reverted', 'yes', 15 );
add_action(
'admin_notices',
function () {
echo '';
}
);
}
}
/**
* Flag a one-shot transient when the merchant turns on Abandoned cart recovery.
*
* `change_feature_enable` fires this action mid-request before the post-save
* redirect, so the actual notice has to render on the next page load. We
* stash a transient here and `maybe_render_abandoned_cart_recovery_enabled_notice`
* picks it up the next time `woocommerce_settings_advanced` fires.
*
* @param string $feature_id Feature being toggled.
* @param bool $is_enabled True when turned on, false when turned off.
*
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
*/
public function flag_abandoned_cart_recovery_enabled_notice( $feature_id, $is_enabled ): void {
if ( 'abandoned_cart_recovery' === $feature_id && $is_enabled ) {
set_transient( 'wc_abandoned_cart_recovery_enabled_notice', 'yes', MINUTE_IN_SECONDS );
}
}
/**
* Render a success notice after the merchant enables Abandoned cart recovery,
* pointing them straight at the email settings page where they actually
* configure it.
*
* Hooks into `woocommerce_settings_advanced` at priority 1, which fires
* inside the settings template right after `WC_Admin_Settings::show_messages()`
* (the "Your settings have been saved." notice) and before the form fields.
* That places our notice in the same visual slot below the tabs, alongside
* the standard save confirmation.
*
* `WC_Admin_Settings::add_message()` would be the cleaner API but escapes
* its input via `esc_html()`, which strips the link tag. Direct echo here
* keeps the markup intact while still matching the surrounding notice style.
*
* @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
*/
public function maybe_render_abandoned_cart_recovery_enabled_notice(): void {
if ( 'yes' !== get_transient( 'wc_abandoned_cart_recovery_enabled_notice' ) ) {
return;
}
delete_transient( 'wc_abandoned_cart_recovery_enabled_notice' );
$settings_url = admin_url( 'admin.php?page=wc-settings&tab=email§ion=wc_email_customer_abandoned_cart_recovery' );
printf(
'',
esc_html__( 'Abandoned cart recovery is enabled.', 'woocommerce' ),
esc_url( $settings_url ),
esc_html__( 'Configure the recovery email →', 'woocommerce' )
);
}
/**
* Check if the email improvements feature is enabled in preview mode in Settings > Emails.
* This is used to force the email improvements feature without affecting shoppers.
*
* @param string $feature_id The feature id.
* @return bool Whether the email improvements feature is enabled in preview mode.
*/
private function is_preview_email_improvements_enabled( string $feature_id ): bool {
if ( 'email_improvements' !== $feature_id ) {
return false;
}
/**
* This filter is documented in templates/emails/email-styles.php
*
* @since 9.9.0
* @param bool $is_email_preview Whether the email is being previewed.
*/
$is_email_preview = apply_filters( 'woocommerce_is_email_preview', false );
if ( $is_email_preview ) {
return get_transient( EmailPreview::TRANSIENT_PREVIEW_EMAIL_IMPROVEMENTS ) === 'yes';
}
return false;
}
}