Skip to content

Commit

Permalink
Use WC built-in Action Scheduler to fetch in-app promotions (#45628)
Browse files Browse the repository at this point in the history
* Use WC built-in ActionScheduler

* Calling `WC_Admin_Marketplace_Promotions::init` immediately from `WC_Admin` constructor. `WC_Admin` is only instantiated in an admin request. If we init marketplace promotions from `woocommerce_init`, we'll be too late to add the callback for the scheduled action that fetches promotion data.

* Changed transient life to 12 hours to match frequency of ActionScheduler action.

* Changelog.

* Updated `fetch_marketplace_promotions` to replace the transient every time it runs. This allows more frequent changes.

* Added filter to allow promotions to be suppressed.

* Fix indentation

* More indentation fix

* Clearing scheduled action if `woocommerce_marketplace_suppress_promotions` filter returns "true".

* Running `clear_scheduled_event` on `init`.

* WP_CLI check

* We were including and instantiating `WC_Admin_Marketplace_Promotions` from the `WC_Admin` constructor. But `WC_Admin` is only instantiated during `is_admin` requests. We also need to respond to cron requests. So we're now including the class from `class-woocommerce.php` if the context is admin or cron, and instantiating it on `init`. This fixes the error in Scheduled Action `action failed via WP Cron: Scheduled action for woocommerce_marketplace_fetch_promotions will not be executed as no callbacks are registered.`

* Linter errors.

* Enhance append_bubble() method

* introduce SCHEDULED_ACTION_INTERVAL

* Make linter happier

* Linter errors.

---------

Co-authored-by: Remi Corson <1649788+corsonr@users.noreply.github.com>
Co-authored-by: And Finally <andfinally@users.noreply.github.com>
  • Loading branch information
3 people committed Mar 20, 2024
1 parent 13ac914 commit 7c7ed0d
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 55 deletions.
@@ -0,0 +1,4 @@
Significance: patch
Type: fix

Using ActionScheduler to schedule fetching of in-app marketplace promotions.
Expand Up @@ -15,8 +15,11 @@
*/
class WC_Admin_Marketplace_Promotions {

const TRANSIENT_NAME = 'woocommerce_marketplace_promotions';
const SCHEDULED_ACTION_HOOK = 'woocommerce_marketplace_fetch_promotions';
const TRANSIENT_NAME = 'woocommerce_marketplace_promotions';
const SCHEDULED_ACTION_HOOK = 'woocommerce_marketplace_fetch_promotions';
const PROMOTIONS_API_URL = 'https://woo.com/wp-json/wccom-extensions/3.0/promotions';
const SCHEDULED_ACTION_INTERVAL = 12 * HOUR_IN_SECONDS;

/**
* The user's locale, for example en_US.
*
Expand All @@ -26,69 +29,72 @@ class WC_Admin_Marketplace_Promotions {

/**
* On all admin pages, schedule an action to fetch promotions data.
* Add menu badge to WooCommerce Extensions item if the promotions
* API requests one.
* Shows notice and adds menu badge to WooCommerce Extensions item
* if the promotions API requests them.
*
* WC_Admin calls this method when it is instantiated during
* is_admin requests.
*
* @return void
*/
public static function init_marketplace_promotions() {
public static function init() {
register_deactivation_hook( WC_PLUGIN_FILE, array( __CLASS__, 'clear_scheduled_event' ) );

/**
* Filter to suppress the requests for and showing of marketplace promotions.
*
* @since 8.8
*/
if ( apply_filters( 'woocommerce_marketplace_suppress_promotions', false ) ) {
add_action( 'init', array( __CLASS__, 'clear_scheduled_event' ), 13 );

return;
}

// Add the callback for our scheduled action.
if ( ! has_action( self::SCHEDULED_ACTION_HOOK, array( __CLASS__, 'fetch_marketplace_promotions' ) ) ) {
add_action( self::SCHEDULED_ACTION_HOOK, array( __CLASS__, 'fetch_marketplace_promotions' ) );
}

if ( self::is_admin_page() ) {
// Schedule the action twice a day, starting now.
if ( false === wp_next_scheduled( self::SCHEDULED_ACTION_HOOK ) ) {
wp_schedule_event( time(), 'twicedaily', self::SCHEDULED_ACTION_HOOK );
}
if ( is_admin() ) {
add_action( 'init', array( __CLASS__, 'schedule_promotion_fetch' ), 12 );
}

self::$locale = ( self::$locale ?? get_user_locale() ) ?? 'en_US';
self::maybe_show_bubble_promotions();
if (
defined( 'DOING_AJAX' ) && DOING_AJAX
|| defined( 'DOING_CRON' ) && DOING_CRON
|| defined( 'WP_CLI' ) && WP_CLI
) {
return;
}

register_deactivation_hook( WC_PLUGIN_FILE, array( __CLASS__, 'clear_scheduled_event' ) );
self::$locale = ( self::$locale ?? get_user_locale() ) ?? 'en_US';
self::maybe_show_bubble_promotions();
}

/**
* Check if the request is for an admin page, and not ajax.
* We may want to add a menu bubble to WooCommerce Extensions
* on any admin page, as the user may view the WooCommerce flyout
* menu.
*
* @return bool
* Schedule the action to fetch promotions data.
*/
private static function is_admin_page(): bool {
if (
( defined( 'DOING_AJAX' ) && DOING_AJAX )
|| ! is_admin()
) {
return false;
public static function schedule_promotion_fetch() {
// Schedule the action twice a day using Action Scheduler.
if ( false === as_has_scheduled_action( self::SCHEDULED_ACTION_HOOK ) ) {
as_schedule_recurring_action( time(), self::SCHEDULED_ACTION_INTERVAL, self::SCHEDULED_ACTION_HOOK );
}

return true;
}

/**
* Get promotions to show in the Woo in-app marketplace.
* Only run on selected pages in the main WooCommerce menu in wp-admin.
* Loads promotions in transient with one day life.
* Get promotions to show in the Woo in-app marketplace and load them into a transient
* with a 12-hour life. Run as a recurring scheduled action.
*
* @return void
*/
public static function fetch_marketplace_promotions() {
$url = 'https://woo.com/wp-json/wccom-extensions/3.0/promotions';
$promotions = get_transient( self::TRANSIENT_NAME );

if ( false !== $promotions ) {
return;
}

// Fetch promotions from the API.
$fetch_options = array(
'auth' => true,
'country' => true,
);
$raw_promotions = WC_Admin_Addons::fetch( $url, $fetch_options );
$raw_promotions = WC_Admin_Addons::fetch( self::PROMOTIONS_API_URL, $fetch_options );

// phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores
if ( is_wp_error( $raw_promotions ) ) {
Expand All @@ -98,6 +104,8 @@ public static function fetch_marketplace_promotions() {
* @since 8.7
*/
do_action( 'woocommerce_page_wc-addons_connection_error', $raw_promotions->get_error_message() );

return;
}

$response_code = (int) wp_remote_retrieve_response_code( $raw_promotions );
Expand All @@ -108,6 +116,8 @@ public static function fetch_marketplace_promotions() {
* @since 8.7
*/
do_action( 'woocommerce_page_wc-addons_connection_error', $response_code );

return;
}

$promotions = json_decode( wp_remote_retrieve_body( $raw_promotions ), true );
Expand All @@ -118,14 +128,14 @@ public static function fetch_marketplace_promotions() {
* @since 8.7
*/
do_action( 'woocommerce_page_wc-addons_connection_error', 'Empty or malformed response' );

return;
}
// phpcs:enable WordPress.NamingConventions.ValidHookName.UseUnderscores

if ( $promotions ) {
// Filter out any expired promotions.
$promotions = self::get_active_promotions( $promotions );
set_transient( self::TRANSIENT_NAME, $promotions, DAY_IN_SECONDS );
}
// Filter out any expired promotions.
$active_promotions = self::get_active_promotions( $promotions );
set_transient( self::TRANSIENT_NAME, $active_promotions, 12 * HOUR_IN_SECONDS );
}

/**
Expand Down Expand Up @@ -246,7 +256,7 @@ function ( $a, $b ) {
*
* @return array
*/
public static function filter_marketplace_menu_items( $menu_items, $promotion = array() ) {
public static function filter_marketplace_menu_items( $menu_items, $promotion = array() ): array {
if ( ! isset( $promotion['menu_item_id'] ) || ! isset( $promotion['content'] ) ) {
return $menu_items;
}
Expand All @@ -266,14 +276,26 @@ public static function filter_marketplace_menu_items( $menu_items, $promotion =
}

/**
* Return the markup for a menu item bubble with a given text.
* Return the markup for a menu item bubble with a given text and optional additional attributes.
*
* @param string $bubble_text Text of bubble.
* @param array $attributes Optional. Additional attributes for the bubble, such as class or style.
*
* @return string
*/
private static function append_bubble( $bubble_text ) {
return ' <span class="awaiting-mod update-plugins remaining-tasks-badge woocommerce-task-list-remaining-tasks-badge">' . esc_html( $bubble_text ) . '</span>';
private static function append_bubble( $bubble_text, $attributes = array() ) {
$default_attributes = array(
'class' => 'awaiting-mod update-plugins remaining-tasks-badge woocommerce-task-list-remaining-tasks-badge',
'style' => '',
);

$attributes = wp_parse_args( $attributes, $default_attributes );
$class_attr = ! empty( $attributes['class'] ) ? sprintf( 'class="%s"', esc_attr( $attributes['class'] ) ) : '';
$style_attr = ! empty( $attributes['style'] ) ? sprintf( 'style="%s"', esc_attr( $attributes['style'] ) ) : '';

$bubble_html = sprintf( ' <span %s %s>%s</span>', $class_attr, $style_attr, esc_html( $bubble_text ) );

return $bubble_html;
}

/**
Expand All @@ -282,7 +304,11 @@ private static function append_bubble( $bubble_text ) {
* @return void
*/
public static function clear_scheduled_event() {
$timestamp = wp_next_scheduled( self::SCHEDULED_ACTION_HOOK );
wp_unschedule_event( $timestamp, self::SCHEDULED_ACTION_HOOK );
as_unschedule_all_actions( self::SCHEDULED_ACTION_HOOK );
}
}

// Fetch list of promotions from Woo.com for WooCommerce admin UI.
if ( ! has_action( 'init', array( 'WC_Admin_Marketplace_Promotions', 'init' ) ) ) {
add_action( 'init', array( 'WC_Admin_Marketplace_Promotions', 'init' ), 11 );
}
6 changes: 0 additions & 6 deletions plugins/woocommerce/includes/admin/class-wc-admin.php
Expand Up @@ -39,9 +39,6 @@ public function __construct() {
if ( isset( $_GET['page'] ) && 'wc-addons' === $_GET['page'] ) {
add_filter( 'admin_body_class', array( 'WC_Admin_Addons', 'filter_admin_body_classes' ) );
}

// Fetch list of promotions from Woo.com for WooCommerce admin UI. We need to fire earlier than admin_init so we can filter menu items.
add_action( 'woocommerce_init', array( 'WC_Admin_Marketplace_Promotions', 'init_marketplace_promotions' ) );
}

/**
Expand Down Expand Up @@ -80,9 +77,6 @@ public function includes() {
// Marketplace suggestions & related REST API.
include_once __DIR__ . '/marketplace-suggestions/class-wc-marketplace-suggestions.php';
include_once __DIR__ . '/marketplace-suggestions/class-wc-marketplace-updater.php';

// Marketplace promotions.
include_once __DIR__ . '/class-wc-admin-marketplace-promotions.php';
}

/**
Expand Down
4 changes: 4 additions & 0 deletions plugins/woocommerce/includes/class-woocommerce.php
Expand Up @@ -681,6 +681,10 @@ public function includes() {
include_once WC_ABSPATH . 'includes/admin/class-wc-admin.php';
}

if ( $this->is_request( 'admin' ) || $this->is_request( 'cron' ) ) {
include_once WC_ABSPATH . 'includes/admin/class-wc-admin-marketplace-promotions.php';
}

// We load frontend includes in the post editor, because they may be invoked via pre-loading of blocks.
$in_post_editor = doing_action( 'load-post.php' ) || doing_action( 'load-post-new.php' );

Expand Down

0 comments on commit 7c7ed0d

Please sign in to comment.