<?php
declare( strict_types=1 );
namespace Automattic\WooCommerce\GoogleListingsAndAds\Menu;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Registerable;
use Automattic\WooCommerce\GoogleListingsAndAds\Infrastructure\Service;
use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\GoogleListingsAndAds\Ads\AdsRecommendationsService;
use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsCampaign;
use Automattic\WooCommerce\GoogleListingsAndAds\Assets\AdminScriptWithBuiltDependenciesAsset;
use Automattic\WooCommerce\GoogleListingsAndAds\Assets\AssetsHandlerInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\ContainerAwareTrait;
use Automattic\WooCommerce\GoogleListingsAndAds\Internal\Interfaces\ContainerAwareInterface;
use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper;
use Automattic\WooCommerce\GoogleListingsAndAds\Value\BuiltScriptDependencyArray;
/**
* Class NotificationManager
*
* @package Automattic\WooCommerce\GoogleListingsAndAds\Menu
*
* Manages the display of a single, aggregated notification pill in the admin menu.
* It relies on a filter to gather the total count from various contributors.
*/
class NotificationManager implements ContainerAwareInterface, Service, Registerable {
use PluginHelper;
use ContainerAwareTrait;
/**
* @var AssetsHandlerInterface
*/
protected $assets_handler;
/**
* NotificationManager constructor.
*
* @param AssetsHandlerInterface $assets_handler
*/
public function __construct( AssetsHandlerInterface $assets_handler ) {
$this->assets_handler = $assets_handler;
}
/**
* Register the service, hooking into admin_menu to display notifications.
*/
public function register(): void {
// Hook into admin_menu with a high priority (e.g., 20) to ensure
// all other menu items have been registered by WooCommerce and other plugins.
add_action( 'admin_menu', [ $this, 'display_aggregated_notification_pill' ], 20 );
add_filter( 'google_for_woocommerce_admin_menu_notification_count', [ $this, 'performance_max_ad_strength_count' ] );
add_filter( 'google_for_woocommerce_admin_menu_notification_count', [ $this, 'raise_budget_recommendations_count' ] );
}
/**
* Register assets.
*
* @return void
*/
private function register_assets(): void {
$notification_manager = new AdminScriptWithBuiltDependenciesAsset(
'notification-manager',
'js/build/notification-manager',
"{$this->get_root_dir()}/js/build/notification-manager.asset.php",
new BuiltScriptDependencyArray(
[
'dependencies' => [],
'version' => $this->get_version(),
]
),
function () {
return PageController::is_admin_page();
}
);
$this->assets_handler->register( $notification_manager );
add_action(
'admin_enqueue_scripts',
function () use ( $notification_manager ) {
if ( ! $this->is_marketing_page() && ! $this->is_analytics_page() ) {
return;
}
$this->assets_handler->enqueue( $notification_manager );
}
);
}
/**
* Determines if the current admin page is a child page within the WooCommerce Marketing section.
* This logic is crucial for deciding where the notification pill should be placed.
*
* @return bool True if the current page is a Marketing child page, false otherwise.
*/
private function is_marketing_page(): bool {
global $pagenow;
$current_page_slug = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
$current_page_path = isset( $_GET['path'] ) ? sanitize_text_field( wp_unslash( $_GET['path'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
$current_post_type = isset( $_GET['post_type'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
$post_type_page = add_query_arg(
[
'post_type' => $current_post_type,
],
$pagenow
);
$page_path_fragment = str_replace(
'page=',
'',
build_query(
[
'page' => $current_page_slug,
'path' => $current_page_path,
]
)
);
$page_controller_pages = PageController::get_instance()->get_pages();
$marketing_menu_slug = Dashboard::MARKETING_MENU_SLUG;
$marketing_menu_pages = array_filter(
$page_controller_pages,
static function ( $page ) use ( $marketing_menu_slug ) {
return isset( $page['parent'] ) && $page['parent'] === $marketing_menu_slug;
}
);
$is_marketing_page = false;
foreach ( $marketing_menu_pages as $page ) {
if ( isset( $page['path'] ) && in_array( $page['path'], [ $post_type_page, $page_path_fragment ], true ) ) {
$is_marketing_page = true;
break;
}
}
return $is_marketing_page;
}
/**
* Displays an aggregated notification pill in the admin menu.
* This method is hooked to 'admin_menu'.
*/
public function display_aggregated_notification_pill(): void {
global $menu, $submenu;
// Initialize the count and apply the filter to get the total aggregated count.
// All parts of the plugin (and other plugins) that need to add to the notification
// should hook into this filter.
$total_notification_count = apply_filters( 'google_for_woocommerce_admin_menu_notification_count', 0 );
// Only proceed if there's at least one notification.
if ( $total_notification_count > 0 ) {
// Register assets.
$this->register_assets();
$badge_html = ' <span class="update-plugins count-' . $total_notification_count . '"><span class="update-count">' . $total_notification_count . '</span></span>';
// Determine if the current page being loaded is within the Marketing section.
$is_on_marketing_child_page = $this->is_marketing_page();
if ( $is_on_marketing_child_page ) {
// If on a Marketing child page, add the pill to the 'Google for WooCommerce' sub-menu item.
// This means the user has the Marketing menu expanded and is viewing one of its sub-pages.
$marketing_parent_slug = Dashboard::MARKETING_MENU_SLUG; // Use constant for parent slug
$google_for_woocommerce_menu_path = Dashboard::PATH; // Use constant for GfW path
if ( isset( $submenu[ $marketing_parent_slug ] ) ) {
foreach ( $submenu[ $marketing_parent_slug ] as $key => $submenu_item ) {
// Use the submenu's slug (index 2) for robustness against translations.
// The slug will contain the path defined by the plugin.
if ( isset( $submenu_item[2] ) && strpos( $submenu_item[2], $google_for_woocommerce_menu_path ) !== false ) {
$submenu[ $marketing_parent_slug ][ $key ][0] .= $badge_html; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
break;
}
}
}
} else {
foreach ( $menu as $key => $menu_item ) {
// Use the top-level menu's slug (index 2) for robustness against translations.
if ( isset( $menu_item[2] ) && Dashboard::MARKETING_MENU_SLUG === $menu_item[2] ) {
$menu[ $key ][0] .= $badge_html; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
break;
}
}
}
}
}
/**
* Returns the initial notification count for the admin menu.
*
* @param int $count The initial count.
* @return int The updated notification count, which is either 1 (if there are recommendations) or 0 (if there are no recommendations).
*/
public function performance_max_ad_strength_count( int $count ): int {
global $wpdb;
$query = $this->container->get( AdsRecommendationsService::class );
$ads_campaign = $this->container->get( AdsCampaign::class );
$campaign = $ads_campaign->get_highest_spend_campaign();
// Return early if there is no highest spend campaign.
if ( empty( $campaign ) || ! isset( $campaign['id'] ) ) {
return $count;
}
// Check IMPROVE_PERFORMANCE_MAX_AD_STRENGTH recommendations for highest spend campaign.
$recommendations = $query->get_recommendations(
[
'types' => [ 'IMPROVE_PERFORMANCE_MAX_AD_STRENGTH' ],
'campaign_id' => $campaign['id'],
]
);
// Return early if there are no recommendations.
if ( empty( $recommendations ) ) {
return $count;
}
// Check recommendation dates and user preference.
$preferences = get_user_meta( get_current_user_id(), "{$wpdb->prefix}persisted_preferences", true );
// If the user has not interacted with a recommendation yet.
if ( ! is_array( $preferences ) || ! isset( $preferences['woocommerce/google-listings-and-ads']['pmax-improve-assets-banner']['actionType'] ) || ! isset( $preferences['woocommerce/google-listings-and-ads']['pmax-improve-assets-banner']['actionTime'] ) ) {
return ++$count;
}
$action_time = $preferences['woocommerce/google-listings-and-ads']['pmax-improve-assets-banner']['actionTime'];
if ( time() > $action_time + ( 30 * DAY_IN_SECONDS ) ) {
return ++$count;
}
return $count;
}
/**
* Returns the count for Raise Budget Recommendations.
*
* @param int $count The initial count.
* @return int The updated notification count for Raise Budget Recommendations.
*/
public function raise_budget_recommendations_count( int $count ): int {
global $wpdb;
$query = $this->container->get( AdsRecommendationsService::class );
$recommendations = $query->get_recommendations(
[
'types' => [
'CAMPAIGN_BUDGET',
'MARGINAL_ROI_CAMPAIGN_BUDGET',
],
'campaign_id' => 0,
]
);
if ( empty( $recommendations ) ) {
return $count;
}
// Check recommendation dates and user preference.
$preferences = get_user_meta( get_current_user_id(), "{$wpdb->prefix}persisted_preferences", true );
// If the user has not interacted with a recommendation yet.
if ( ! is_array( $preferences ) || ! isset( $preferences['woocommerce/google-listings-and-ads']['raise-budget-recommendation-banner']['actionType'] ) || ! isset( $preferences['woocommerce/google-listings-and-ads']['raise-budget-recommendation-banner']['actionTime'] ) ) {
return ++$count;
}
$action_time = $preferences['woocommerce/google-listings-and-ads']['raise-budget-recommendation-banner']['actionTime'];
if ( time() > $action_time + ( 30 * DAY_IN_SECONDS ) ) {
return ++$count;
}
return $count;
}
/**
* Determines if the current admin page is the Analytics.
*
* @return bool True if the current menu item is the Analytics menu or one of it's sub menus.
*/
private function is_analytics_page(): bool {
$current_page_slug = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
if ( $current_page_slug !== 'wc-admin' ) {
return false;
}
$current_page_path = isset( $_GET['path'] ) ? sanitize_text_field( wp_unslash( $_GET['path'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
$parts = explode( '/', ltrim( $current_page_path, '/' ) );
if ( isset( $parts[0] ) && $parts[0] === 'analytics' ) {
return true;
}
return false;
}
}