From 0704c8415bfdc0cb299c4ec5e6f8e728991c1995 Mon Sep 17 00:00:00 2001 From: "Daniel L. Iser" Date: Sat, 15 Aug 2026 04:53:43 -0400 Subject: [PATCH] feat: make Pro guidance metadata-only --- CHANGELOG.md | 6 + README.md | 14 +- composer.json | 2 +- src/Controllers/Admin/ProUpsell.php | 388 ---------------------------- src/Plugin/Core.php | 107 ++++++-- 5 files changed, 104 insertions(+), 413 deletions(-) delete mode 100644 src/Controllers/Admin/ProUpsell.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0126acb..0452d18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.1.0 - 2026-08-15 + +- Remove framework-rendered Pro offers; Popup Maker Core is now the sole renderer and decision-maker. +- Register optional `pro_feature` metadata with Core using only local plugin identifiers. +- Keep the bundled Core catalog authoritative so old extensions do not need an update. + ## 1.0.6 - 2026-05-27 - Use "+" instead of "plus" in Pro panel upsell copy. diff --git a/README.md b/README.md index a8b529a..ae8b6c3 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Releases are tagged semver (`v1.0.0`, etc.). - `Plugin\Core` — container bootstrap, `$this->core`, extension registration, license init - `Services\License` — wraps `PUM_Extension_License` - `Controllers\Assets` — DEWP/webpack asset registration from config -- `Controllers\Admin\ProUpsell` — Pro migration upsell when Pro is not active +- local Pro-feature metadata registration for Core-owned legacy guidance - `Plugin\Controller` — controller base ## Extension config keys @@ -39,8 +39,16 @@ Releases are tagged semver (`v1.0.0`, etc.). | Key | Purpose | |-----|---------| | `asset_packages` | Webpack package definitions for `Assets` controller | -| `pro_upsell.feature_name` | Feature name in admin notice copy | -| `pro_upsell.utm_medium` | UTM medium for `generate_upgrade_url()` | +| `pro_feature.slug` | Stable Core legacy-catalog key; defaults to the extension slug | +| `pro_feature.feature_name` | Customer-facing feature name registered with Core | +| `pro_feature.priority` | Optional aggregation order | +| `pro_feature.plugin_basenames` | Optional historical basenames in addition to the current basename | +| `pro_feature.license_shortnames` | Optional historical local license option prefixes | +| `pro_feature.classes` / `constants` | Optional stable identifiers for active old versions | + +The framework does not render offers or fetch offer data. Popup Maker Core owns +the bundled backwards-compatibility catalog, local state decisions, rendering, +and dismissal storage. The old `pro_upsell` config key is intentionally ignored. Standard plugin keys (`slug`, `edd_id`, `name`, `version`, `text_domain`, `basename`, etc.) are required. diff --git a/composer.json b/composer.json index 371ae1c..8ef25bb 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "popupmaker/extension-framework", "description": "Shared PHP framework for Popup Maker standalone Pro-tier extensions.", - "version": "1.0.6", + "version": "1.1.0", "type": "library", "license": "GPL-2.0-or-later", "keywords": [ diff --git a/src/Controllers/Admin/ProUpsell.php b/src/Controllers/Admin/ProUpsell.php deleted file mode 100644 index 2984301..0000000 --- a/src/Controllers/Admin/ProUpsell.php +++ /dev/null @@ -1,388 +0,0 @@ -is_pro_active() ) { - return; - } - - add_action( 'admin_notices', [ $this, 'admin_notice' ] ); - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_dismiss_script' ] ); - add_filter( 'plugin_row_meta', [ $this, 'plugin_row_meta' ], 10, 2 ); - add_filter( 'pum_alert_list', [ $this, 'register_panel_notification' ] ); - } - - /** - * Whether Popup Maker Pro is active. - * - * @return bool - */ - protected function is_pro_active() { - if ( function_exists( '\PopupMaker\plugin' ) ) { - return \PopupMaker\plugin()->is_pro_active(); - } - - return class_exists( '\PopupMaker\Pro\Plugin\Core' ); - } - - /** - * Pro features bundled in Popup Maker Pro, highest value first. - * - * Keys match retired extension slugs where applicable. - * - * @return array Slug => customer-facing label. - */ - protected function get_pro_features_by_priority() { - return [ - 'analytics' => 'Analytics', - 'exit-intent-popups' => 'Exit Intent', - 'advanced-targeting-conditions' => 'Advanced Targeting', - 'scheduling' => 'Scheduling', - 'scroll-triggered-popups' => 'Scroll Triggers', - 'advanced-theme-builder' => 'Theme Builder', - 'split-testing' => 'Split Testing', - 'forced-interaction' => 'Forced Interaction', - 'call-to-actions' => 'Call to Actions', - ]; - } - - /** - * Resolve the catalog slug for the active extension, if any. - * - * @return string|null - */ - protected function get_current_pro_feature_slug() { - $slug = sanitize_key( $this->container->get( 'slug' ) ); - $catalog = $this->get_pro_features_by_priority(); - - if ( isset( $catalog[ $slug ] ) ) { - return $slug; - } - - $feature_name = $this->get_upsell_config()['feature_name']; - - foreach ( $catalog as $catalog_slug => $label ) { - if ( 0 === strcasecmp( $label, $feature_name ) ) { - return $catalog_slug; - } - } - - return null; - } - - /** - * Top Pro features to highlight, excluding what this extension already provides. - * - * @param int $limit Number of features to return. - * @return array - */ - protected function get_highlighted_pro_features( $limit = 4 ) { - $catalog = $this->get_pro_features_by_priority(); - $current = $this->get_current_pro_feature_slug(); - $features = []; - - foreach ( $catalog as $slug => $label ) { - if ( $slug === $current ) { - continue; - } - - $features[] = $label; - - if ( count( $features ) >= $limit ) { - break; - } - } - - return $features; - } - - /** - * Format a list of feature names for readable copy. - * - * @param array $features Feature names. - * @return string - */ - protected function format_feature_list( $features ) { - $features = array_values( $features ); - - if ( empty( $features ) ) { - return ''; - } - - if ( 1 === count( $features ) ) { - return $features[0]; - } - - $last = array_pop( $features ); - - return implode( ', ', $features ) . ', and ' . $last; - } - - /** - * Upsell config with defaults. - * - * @return array - */ - protected function get_upsell_config() { - $config = (array) $this->container->get( 'pro_upsell' ); - - return wp_parse_args( - $config, - [ - 'feature_name' => $this->container->get( 'name' ), - 'utm_medium' => 'extension-' . $this->container->get( 'slug' ), - ] - ); - } - - /** - * Alert code for the admin notice banner. - * - * @return string - */ - protected function get_admin_notice_code() { - return 'pm_extension_pro_upsell_' . sanitize_key( $this->container->get( 'slug' ) ) . '_admin'; - } - - /** - * Alert code for the notifications panel. - * - * @return string - */ - protected function get_panel_alert_code() { - return 'pm_extension_pro_upsell_' . sanitize_key( $this->container->get( 'slug' ) ) . '_panel'; - } - - /** - * Whether an alert code has been dismissed. - * - * @param string $code Alert code. - * @return bool - */ - protected function is_alert_dismissed( $code ) { - return class_exists( 'PUM_Utils_Alerts' ) && \PUM_Utils_Alerts::has_dismissed_alert( $code ); - } - - /** - * Whether upsell surfaces can render for the current user. - * - * @return bool - */ - protected function can_show() { - if ( ! current_user_can( 'manage_options' ) ) { - return false; - } - - if ( ! function_exists( '\PopupMaker\generate_upgrade_url' ) ) { - return false; - } - - return true; - } - - /** - * Whether the admin notice should render. - * - * @return bool - */ - protected function should_show_admin_notice() { - return $this->can_show() && ! $this->is_alert_dismissed( $this->get_admin_notice_code() ); - } - - /** - * Whether the panel notification should render. - * - * @return bool - */ - protected function should_show_panel_notification() { - return $this->can_show() && ! $this->is_alert_dismissed( $this->get_panel_alert_code() ); - } - - /** - * Upgrade URL for upsell CTAs. - * - * @param string $utm_content UTM content slug. - * @return string - */ - protected function get_upgrade_url( $utm_content ) { - $upsell = $this->get_upsell_config(); - - return \PopupMaker\generate_upgrade_url( - $upsell['utm_medium'], - 'migrate-to-pro', - $utm_content - ); - } - - /** - * Admin notice on Popup Maker screens. - * - * @return void - */ - public function admin_notice() { - if ( ! $this->should_show_admin_notice() || ! function_exists( 'pum_is_admin_page' ) || ! pum_is_admin_page() ) { - return; - } - - $upsell = $this->get_upsell_config(); - $url = $this->get_upgrade_url( 'admin-notice' ); - $highlight_features = $this->format_feature_list( $this->get_highlighted_pro_features( 3 ) ); - - printf( - '

%3$s

', - esc_attr( $this->container->get( 'slug' ) ), - esc_attr( $this->get_admin_notice_code() ), - wp_kses_post( - sprintf( - /* translators: %1$s: feature name, %2$s: opening anchor, %3$s: closing anchor, %4$s: other bundled pro features */ - __( '%1$s is included in %2$sPopup Maker Pro%3$s along with %4$s, and more.', $this->container->get( 'text_domain' ) ), - esc_html( $upsell['feature_name'] ), - '', - '', - esc_html( $highlight_features ) - ) - ) - ); - } - - /** - * Persist admin notice dismissal via core alert storage. - * - * @return void - */ - public function enqueue_dismiss_script() { - if ( ! $this->should_show_admin_notice() || ! function_exists( 'pum_is_admin_page' ) || ! pum_is_admin_page() ) { - return; - } - - wp_enqueue_script( 'jquery' ); - - $code = esc_js( $this->get_admin_notice_code() ); - $nonce = esc_js( wp_create_nonce( 'pum_alerts_action' ) ); - - wp_add_inline_script( - 'jquery', - "jQuery( function ( $ ) { - $( document ).on( 'click', '.notice[data-pum-upsell] .notice-dismiss', function () { - $.post( ajaxurl, { - action: 'pum_alerts_action', - nonce: '{$nonce}', - code: '{$code}', - pum_dismiss_alert: 'dismiss' - } ); - } ); - } );", - 'after' - ); - } - - /** - * Register a Pro upsell in the core notifications panel. - * - * @param array> $alerts Registered alerts. - * @return array> - */ - public function register_panel_notification( $alerts ) { - if ( ! is_array( $alerts ) || ! $this->should_show_panel_notification() ) { - return $alerts; - } - - $upsell = $this->get_upsell_config(); - $text_domain = $this->container->get( 'text_domain' ); - - $alerts[] = [ - 'code' => $this->get_panel_alert_code(), - 'category' => 'offer', - 'priority' => 65, - 'dismissible' => true, - 'type' => 'info', - 'title' => __( 'More features. Less money.', $text_domain ), - 'message' => $this->get_panel_message( $upsell ), - 'subtitle' => __( 'under $100/yr', $text_domain ), - 'icon' => 'awards', - 'actions' => [ - [ - 'text' => __( 'See what\'s in Pro', $text_domain ), - 'type' => 'link', - 'action' => '', - 'href' => $this->get_upgrade_url( 'notifications-panel' ), - 'primary' => true, - 'external' => true, - ], - [ - 'text' => __( 'Not now', $text_domain ), - 'type' => 'action', - 'action' => 'dismiss', - 'expires' => '30 days', - ], - ], - ]; - - return $alerts; - } - - /** - * Richer Pro value message for the notifications panel. - * - * @param array $upsell Upsell config. - * @return string - */ - protected function get_panel_message( $upsell ) { - $highlight_features = $this->format_feature_list( $this->get_highlighted_pro_features( 4 ) ); - - return sprintf( - /* translators: 1: current extension feature name, 2: top bundled pro feature names */ - __( - '%1$s + %2$s and 10+ more pro features — bundled in Popup Maker Pro for less than buying extensions à la carte.', - $this->container->get( 'text_domain' ) - ), - esc_html( $upsell['feature_name'] ), - esc_html( $highlight_features ) - ); - } - - /** - * Plugin row meta link. - * - * @param array $links Links. - * @param string $plugin_file Plugin file. - * @return array - */ - public function plugin_row_meta( $links, $plugin_file ) { - if ( $this->container->get( 'basename' ) !== $plugin_file || ! function_exists( '\PopupMaker\generate_upgrade_url' ) ) { - return $links; - } - - $links[] = sprintf( - '%s', - esc_url( $this->get_upgrade_url( 'plugins-list' ) ), - esc_html__( 'Upgrade to Pro', $this->container->get( 'text_domain' ) ) - ); - - return $links; - } -} diff --git a/src/Plugin/Core.php b/src/Plugin/Core.php index a5fef74..a925769 100644 --- a/src/Plugin/Core.php +++ b/src/Plugin/Core.php @@ -7,7 +7,6 @@ namespace PopupMaker\ExtensionFramework\Plugin; -use PopupMaker\ExtensionFramework\Controllers\Admin\ProUpsell; use PopupMaker\ExtensionFramework\Controllers\Assets; use PopupMaker\ExtensionFramework\Services\License; @@ -27,6 +26,7 @@ public function __construct( $config ) { parent::__construct( $config ); add_filter( 'pum_enabled_extensions', [ $this, 'register_extension' ] ); + add_filter( 'popup_maker/legacy_extension_catalog', [ $this, 'register_pro_feature_metadata' ] ); add_action( 'init', [ $this, 'load_textdomain' ] ); $this->get( 'license' )->init(); @@ -64,28 +64,9 @@ protected function registered_controllers() { * @return array */ protected function framework_controllers() { - $controllers = [ + return [ 'Assets' => new Assets( $this ), ]; - - if ( $this->should_register_pro_upsell() ) { - $controllers['Admin\\ProUpsell'] = new ProUpsell( $this ); - } - - return $controllers; - } - - /** - * Whether to register the Pro migration upsell controller. - * - * @return bool - */ - protected function should_register_pro_upsell() { - if ( ! $this->offsetExists( 'pro_upsell' ) ) { - return true; - } - - return false !== $this->get( 'pro_upsell' ); } /** @@ -107,6 +88,90 @@ public function register_extension( $extensions ) { return $extensions; } + /** + * Register local feature metadata for Core-owned Legacy to Pro guidance. + * + * The framework intentionally does not render offers. Core supplies the + * bundled backwards-compatibility catalog and remains the sole renderer and + * decision-maker. This registration only enhances Core's local identifiers + * when an updated extension is available. + * + * @param array> $catalog Core catalog. + * @return array> + */ + public function register_pro_feature_metadata( $catalog ) { + if ( ! is_array( $catalog ) ) { + $catalog = []; + } + + if ( ! $this->offsetExists( 'pro_feature' ) || false === $this->get( 'pro_feature' ) ) { + return $catalog; + } + + $config = (array) $this->get( 'pro_feature' ); + $slug = sanitize_key( isset( $config['slug'] ) ? $config['slug'] : $this->get( 'slug' ) ); + $feature_name = isset( $config['feature_name'] ) + ? trim( wp_strip_all_tags( (string) $config['feature_name'] ) ) + : trim( wp_strip_all_tags( (string) $this->get( 'name' ) ) ); + + if ( '' === $slug || '' === $feature_name ) { + return $catalog; + } + + $record = isset( $catalog[ $slug ] ) && is_array( $catalog[ $slug ] ) + ? $catalog[ $slug ] + : []; + + $record['feature_name'] = $feature_name; + $record['priority'] = isset( $config['priority'] ) + ? (int) $config['priority'] + : (int) ( $record['priority'] ?? 50 ); + + $arrays = [ + 'plugin_basenames' => array_merge( + [ (string) $this->get( 'basename' ) ], + (array) ( $config['plugin_basenames'] ?? [] ) + ), + 'license_shortnames' => array_merge( + [ $this->get_license_shortname( (string) $this->get( 'name' ) ) ], + (array) ( $config['license_shortnames'] ?? [] ) + ), + 'classes' => (array) ( $config['classes'] ?? [] ), + 'constants' => (array) ( $config['constants'] ?? [] ), + ]; + + foreach ( $arrays as $key => $values ) { + $record[ $key ] = array_values( + array_unique( + array_filter( + array_merge( (array) ( $record[ $key ] ?? [] ), $values ), + 'is_string' + ) + ) + ); + } + + $catalog[ $slug ] = $record; + + return $catalog; + } + + /** + * Build the historical Core extension-license option prefix. + * + * @param string $item_name Extension/EDD item name. + * @return string + */ + private function get_license_shortname( $item_name ) { + $normalized = preg_replace( + '/[^a-zA-Z0-9_\s]/', + '', + str_replace( ' ', '_', strtolower( $item_name ) ) + ); + + return 'popmake_' . (string) $normalized; + } + /** * Load text domain. *