catalog = $catalog ?? new Catalog(); } /** * Register hooks for the recommended-plugins menu. * * @since 4.9.0 */ public function hooks() { add_action( 'admin_init', [ $this, 'register_pages' ] ); add_action( 'activated_plugin', [ $this, 'record_activation' ], 10, 2 ); } /** * Instantiate every product landing page so each can register its own * AJAX and asset hooks. * * Runs on `admin_init` rather than synchronously: the catalog labels are * translated, and building it before `init` triggers a textdomain-too-early * notice on Pro, where the email log boots the admin during `plugins_loaded`. * * @since 4.9.0 */ public function register_pages() { foreach ( $this->get_products() as $product ) { if ( class_exists( $product['page_class'] ) ) { new $product['page_class'](); } } } /** * Register the rotating submenu item for the currently surfaced product. * * Called by Area during its menu build so the item lands in the right * position; does nothing once every product is adopted. * * @since 4.9.0 * * @param string $access_capability Capability required to view the page. */ public function add_submenu_item( $access_capability ) { $product = $this->get_current_product(); if ( $product === null ) { return; } add_submenu_page( Area::SLUG, $product['label'], $product['label'], $access_capability, $product['page_class']::SLUG, [ $this, 'render_current_page' ] ); $short_slug = substr( $product['page_class']::SLUG, strlen( Area::SLUG . '-' ) ); if ( ! in_array( $short_slug, Area::$pages_registered, true ) ) { Area::$pages_registered[] = $short_slug; } } /** * Render the landing page for the currently surfaced product. * * @since 4.9.0 */ public function render_current_page() { $product = $this->get_current_product(); if ( $product === null ) { return; } $page = new $product['page_class'](); // The id is required: all `wpms:` Tailwind utilities are build-scoped // under `#wp-mail-smtp`, so the landing markup must live beneath it. echo '
'; $page->output(); echo '
'; } /** * Ordered product catalog. * * @since 4.9.0 * * @return array> */ public function get_products() { $products = [ [ 'slug' => 'wpconsent', 'label' => esc_html__( 'Privacy Compliance', 'wp-mail-smtp' ), 'page_class' => Pages\WPConsent::class, 'activation_option' => 'wpconsent_activated', ], [ 'slug' => 'activelayer', 'label' => esc_html__( 'Spam Protection', 'wp-mail-smtp' ), 'page_class' => Pages\ActiveLayer::class, 'activation_option' => '', // No activation-time option exposed by the plugin. ], [ 'slug' => 'duplicator', 'label' => esc_html__( 'Backups', 'wp-mail-smtp' ), 'page_class' => Pages\Duplicator::class, 'activation_option' => 'duplicator_install_info', // migration reads ['time']. ], [ 'slug' => 'wpvibe', 'label' => esc_html__( 'AI MCP', 'wp-mail-smtp' ), 'page_class' => Pages\WPVibe::class, 'activation_option' => '', // No activation-time option exposed by the plugin. ], [ 'slug' => 'universally', 'label' => esc_html__( 'Translations', 'wp-mail-smtp' ), 'page_class' => Pages\Universally::class, 'activation_option' => '', // No activation-time option exposed by the plugin. ], [ 'slug' => 'wpcode', 'label' => esc_html__( 'Code Snippets', 'wp-mail-smtp' ), 'page_class' => Pages\WPCode::class, 'activation_option' => 'ihaf_activated', // migration reads ['wpcode']. ], ]; foreach ( $products as $index => $product ) { $plugin = $this->catalog->get( $product['slug'] ); if ( $plugin === null ) { continue; } // `name`, `plugin` and `plugin_pro` shipped in this array before the // catalog owned them, so they stay in the returned shape. $products[ $index ]['name'] = $plugin->get_name(); $products[ $index ]['plugin'] = $plugin->get_basename(); if ( $plugin->get_basename_pro() !== '' ) { $products[ $index ]['plugin_pro'] = $plugin->get_basename_pro(); } } return $products; } /** * The plugin behind a rotation slug. * * @since 4.10.0 * * @param string $slug Rotation product slug. * * @return PartnerPlugin|null */ public function get_plugin( $slug ): ?PartnerPlugin { return $this->catalog->get( $slug ); } /** * Record first activation time of a catalog plugin (write-once). * * @since 4.9.0 * * @param string $plugin Plugin file relative to the plugins dir. * @param bool $network_wide Whether activation is network-wide. */ public function record_activation( $plugin, $network_wide ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $catalog_plugin = $this->catalog->get_by_basename( $plugin ); if ( $catalog_plugin === null ) { return; } $slug = $catalog_plugin->get_slug(); if ( ! in_array( $slug, wp_list_pluck( $this->get_products(), 'slug' ), true ) ) { return; } $activated = (array) get_option( self::ACTIVATED_OPTION, [] ); if ( isset( $activated[ $slug ] ) ) { return; } $activated[ $slug ] = time(); update_option( self::ACTIVATED_OPTION, $activated ); } /** * The product to surface right now, or null once every product is adopted. * * @since 4.9.0 * * @return array|null */ public function get_current_product() { $products = $this->get_products(); $activated = (array) get_option( self::ACTIVATED_OPTION, [] ); $now = time(); foreach ( $products as $product ) { $timestamp = isset( $activated[ $product['slug'] ] ) ? (int) $activated[ $product['slug'] ] : 0; if ( $timestamp === 0 || ( $now - $timestamp ) < self::ADOPTED_AFTER ) { return $product; } } // Everything adopted: surface nothing. The standard "About Us" submenu // remains as the trailing item. return null; } }