diff --git a/admin/src/Analytics/Analytics.php b/admin/src/Analytics/Analytics.php index b0fe807..0ab9a9d 100644 --- a/admin/src/Analytics/Analytics.php +++ b/admin/src/Analytics/Analytics.php @@ -1,6 +1,8 @@ "Role Editor", + "activity-log" => "Activity Log", + "database-explorer" => "Database Explorer", + "analytics" => "Analytics", + "white-label" => "White Label", + "remote-site-switcher" => "Remote Site Switcher", + ]; + + public static function is_active() + { + $key = trim((string) Settings::get("license_key", "")); + return $key !== ""; + } + + /** + * True for any feature not in PAID_FEATURES (nothing to gate) or when a + * license is active; false only when the feature is paid and unlicensed. + */ + public static function is_feature_available($feature) + { + if (!isset(self::PAID_FEATURES[$feature])) { + return true; + } + return self::is_active(); + } + + public static function feature_label($feature) + { + return isset(self::PAID_FEATURES[$feature]) ? self::PAID_FEATURES[$feature] : ""; + } +} diff --git a/admin/src/Pages/SubApps.php b/admin/src/Pages/SubApps.php index 016652e..d950288 100644 --- a/admin/src/Pages/SubApps.php +++ b/admin/src/Pages/SubApps.php @@ -2,6 +2,7 @@ namespace AuroraAdmin\Pages; use AuroraAdmin\Utility\Assets; +use AuroraAdmin\Licensing\License; defined("ABSPATH") || exit(); @@ -31,14 +32,19 @@ defined("ABSPATH") || exit(); */ class SubApps { + // "feature" ties a page to a License::PAID_FEATURES key — null means the + // page is free. Locked pages still register normally (real capability, + // real URL) but render() swaps in an upgrade prompt instead of the real + // feature when unlicensed, and maybe_enqueue() loads that prompt's tiny + // JS bundle instead of the real (much heavier) feature bundle. const PAGES = [ - "aurora-admin-menu-creator" => ["title" => "Menu Creator", "entry" => "menu-creator"], - "aurora-admin-notices" => ["title" => "Admin Notices", "entry" => "admin-notices"], - "aurora-admin-roles" => ["title" => "Role Editor", "entry" => "role-editor"], - "aurora-admin-activity-log" => ["title" => "Activity Log", "entry" => "activity-log"], - "aurora-admin-database" => ["title" => "Database Explorer", "entry" => "database-explorer"], - "aurora-admin-plugins" => ["title" => "Installed Plugins", "entry" => "plugins"], - "aurora-admin-comments" => ["title" => "Comments", "entry" => "comments"], + "aurora-admin-menu-creator" => ["title" => "Menu Creator", "entry" => "menu-creator", "feature" => null], + "aurora-admin-notices" => ["title" => "Admin Notices", "entry" => "admin-notices", "feature" => null], + "aurora-admin-roles" => ["title" => "Role Editor", "entry" => "role-editor", "feature" => "role-editor"], + "aurora-admin-activity-log" => ["title" => "Activity Log", "entry" => "activity-log", "feature" => "activity-log"], + "aurora-admin-database" => ["title" => "Database Explorer", "entry" => "database-explorer", "feature" => "database-explorer"], + "aurora-admin-plugins" => ["title" => "Installed Plugins", "entry" => "plugins", "feature" => null], + "aurora-admin-comments" => ["title" => "Comments", "entry" => "comments", "feature" => null], ]; private static $hook_suffixes = []; @@ -92,7 +98,13 @@ class SubApps return; } - Assets::enqueue("aurora-admin-" . self::PAGES[$slug]["entry"], "entries/" . self::PAGES[$slug]["entry"] . ".js"); + $page = self::PAGES[$slug]; + if ($page["feature"] && !License::is_feature_available($page["feature"])) { + Assets::enqueue("aurora-admin-locked-feature", "entries/locked-feature.js"); + return; + } + + Assets::enqueue("aurora-admin-" . $page["entry"], "entries/" . $page["entry"] . ".js"); } public static function redirect_native_pages() @@ -118,6 +130,15 @@ class SubApps return; } + $feature = self::PAGES[$page]["feature"]; + if ($feature && !License::is_feature_available($feature)) { + $data = ["feature" => License::feature_label($feature)]; + ?> +
+ roles as $slug => $info) { $roles[] = ["value" => $slug, "label" => translate_user_role($info["name"])]; diff --git a/admin/src/Rest/AnalyticsData.php b/admin/src/Rest/AnalyticsData.php index 93dbe75..6619633 100644 --- a/admin/src/Rest/AnalyticsData.php +++ b/admin/src/Rest/AnalyticsData.php @@ -2,6 +2,7 @@ namespace AuroraAdmin\Rest; use AuroraAdmin\Analytics\Analytics; +use AuroraAdmin\Licensing\License; defined("ABSPATH") || exit(); @@ -22,7 +23,7 @@ class AnalyticsData "methods" => "GET", "callback" => [self::class, "handle"], "permission_callback" => function () { - return current_user_can("read"); + return current_user_can("read") && License::is_feature_available("analytics"); }, ]); } diff --git a/admin/src/Roles/RolesData.php b/admin/src/Roles/RolesData.php index e10c426..b359ce0 100644 --- a/admin/src/Roles/RolesData.php +++ b/admin/src/Roles/RolesData.php @@ -57,6 +57,13 @@ class RolesData public static function list_roles() { + // get_editable_roles() lives in wp-admin/includes/user.php, which WP + // only auto-loads for actual wp-admin page requests — REST API requests + // (this endpoint) don't get it for free, so it's undefined there + // without this. + if (!function_exists("get_editable_roles")) { + require_once ABSPATH . "wp-admin/includes/user.php"; + } $editable = get_editable_roles(); $all_caps = self::all_known_capabilities(); diff --git a/admin/src/Shell/Shell.php b/admin/src/Shell/Shell.php index 65cc9e8..88a58ea 100644 --- a/admin/src/Shell/Shell.php +++ b/admin/src/Shell/Shell.php @@ -3,6 +3,7 @@ namespace AuroraAdmin\Shell; use AuroraAdmin\Utility\Assets; use AuroraAdmin\Options\Settings; +use AuroraAdmin\Licensing\License; defined("ABSPATH") || exit(); @@ -207,7 +208,7 @@ class Shell "currentUrl" => esc_url_raw(self::current_admin_url()), // Secrets stripped — this is serialized for every admin user. "settings" => Settings::get_safe(), - "canSwitchSites" => current_user_can($switcher_cap), + "canSwitchSites" => current_user_can($switcher_cap) && License::is_feature_available("remote-site-switcher"), "menu" => MenuSerializer::serialize(), "auroraNav" => self::aurora_nav(), ]; @@ -234,12 +235,12 @@ class Shell private static function aurora_nav() { $items = [ - ["id" => "aurora-admin", "title" => __("Settings", "aurora-admin"), "icon" => "dashicons-admin-generic"], - ["id" => "aurora-admin-menu-creator", "title" => __("Menu Creator", "aurora-admin"), "icon" => "dashicons-menu-alt"], - ["id" => "aurora-admin-notices", "title" => __("Admin Notices", "aurora-admin"), "icon" => "dashicons-megaphone"], - ["id" => "aurora-admin-roles", "title" => __("Role Editor", "aurora-admin"), "icon" => "dashicons-groups"], - ["id" => "aurora-admin-activity-log", "title" => __("Activity Log", "aurora-admin"), "icon" => "dashicons-clock"], - ["id" => "aurora-admin-database", "title" => __("Database Explorer", "aurora-admin"), "icon" => "dashicons-database"], + ["id" => "aurora-admin", "title" => __("Settings", "aurora-admin"), "icon" => "dashicons-admin-generic", "feature" => null], + ["id" => "aurora-admin-menu-creator", "title" => __("Menu Creator", "aurora-admin"), "icon" => "dashicons-menu-alt", "feature" => null], + ["id" => "aurora-admin-notices", "title" => __("Admin Notices", "aurora-admin"), "icon" => "dashicons-megaphone", "feature" => null], + ["id" => "aurora-admin-roles", "title" => __("Role Editor", "aurora-admin"), "icon" => "dashicons-groups", "feature" => "role-editor"], + ["id" => "aurora-admin-activity-log", "title" => __("Activity Log", "aurora-admin"), "icon" => "dashicons-clock", "feature" => "activity-log"], + ["id" => "aurora-admin-database", "title" => __("Database Explorer", "aurora-admin"), "icon" => "dashicons-database", "feature" => "database-explorer"], ]; $nav = []; @@ -252,6 +253,10 @@ class Shell "title" => $item["title"], "url" => esc_url_raw(admin_url("admin.php?page=" . $item["id"])), "icon" => ["type" => "dashicon", "value" => $item["icon"]], + // Still a real, navigable link when locked — SubApps::render() shows + // an upgrade prompt there instead of the real feature. This flag + // just drives the small lock badge in the sidebar (Sidebar.vue). + "locked" => $item["feature"] && !License::is_feature_available($item["feature"]), ]; } diff --git a/admin/src/WhiteLabel/WhiteLabel.php b/admin/src/WhiteLabel/WhiteLabel.php index e9464e9..2286a5c 100644 --- a/admin/src/WhiteLabel/WhiteLabel.php +++ b/admin/src/WhiteLabel/WhiteLabel.php @@ -2,6 +2,7 @@ namespace AuroraAdmin\WhiteLabel; use AuroraAdmin\Options\Settings; +use AuroraAdmin\Licensing\License; defined("ABSPATH") || exit(); @@ -10,6 +11,10 @@ defined("ABSPATH") || exit(); * - admin_favicon: replaces the favicon across wp-admin * - plugin_name: renames the plugin in the plugins list and the admin menu * - text_replacements: find/replace pairs applied to translated admin text + * + * Paid feature — the settings fields stay visible either way (so an admin + * can pre-configure before upgrading), but none of the hooks below register + * without a license, so nothing actually applies yet. */ class WhiteLabel { @@ -18,6 +23,10 @@ class WhiteLabel public function __construct() { + if (!License::is_feature_available("white-label")) { + return; + } + add_action("admin_head", [self::class, "favicon"]); add_filter("all_plugins", [self::class, "rename_plugin"]); add_action("admin_menu", [self::class, "rename_menu"], 999); diff --git a/app/src/entries/locked-feature.js b/app/src/entries/locked-feature.js new file mode 100644 index 0000000..7b0977a --- /dev/null +++ b/app/src/entries/locked-feature.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import UpgradeNotice from '@/pages/_shared/UpgradeNotice.vue'; + +const mountEl = document.querySelector('[data-aurora-locked-feature]'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraLockedFeature || '{}'); + createApp(UpgradeNotice, { feature: data.feature, description: data.description }).mount(mountEl); +} diff --git a/app/src/pages/_shared/UpgradeNotice.vue b/app/src/pages/_shared/UpgradeNotice.vue new file mode 100644 index 0000000..ad812a5 --- /dev/null +++ b/app/src/pages/_shared/UpgradeNotice.vue @@ -0,0 +1,81 @@ + + + ++ {{ description || `Upgrade to Aurora Admin Pro to unlock ${feature}.` }} +
+ + Enter a license key + +{{ field.description }}
diff --git a/app/src/pages/settings/settingsConfig.js b/app/src/pages/settings/settingsConfig.js index 3e2acb7..79293de 100644 --- a/app/src/pages/settings/settingsConfig.js +++ b/app/src/pages/settings/settingsConfig.js @@ -42,7 +42,7 @@ export const settingsCategories = [ type: 'text', label: 'Remote Site Switcher Capability', description: - 'WordPress capability required to see the remote site switcher in the toolbar. Defaults to "manage_options" (admin users). Users with this capability or below will see the switcher.', + 'Pro feature — requires a license. WordPress capability required to see the remote site switcher in the toolbar. Defaults to "manage_options" (admin users). Users with this capability or below will see the switcher.', placeholder: 'manage_options', }, { @@ -111,7 +111,7 @@ export const settingsCategories = [ { id: 'whitelabel', title: 'White Label', - subtitle: 'Rebrand and customize interface text and icons', + subtitle: 'Rebrand and customize interface text and icons (Pro feature — requires a license)', icon: 'dashicons-tag', fields: [ { @@ -198,7 +198,7 @@ export const settingsCategories = [ { id: 'analytics', title: 'Analytics', - subtitle: 'Built-in analytics and tracking settings', + subtitle: 'Built-in analytics and tracking settings (Pro feature — requires a license)', icon: 'dashicons-chart-bar', fields: [ { diff --git a/app/src/shell/components/Sidebar.vue b/app/src/shell/components/Sidebar.vue index c6ba122..bef170b 100644 --- a/app/src/shell/components/Sidebar.vue +++ b/app/src/shell/components/Sidebar.vue @@ -185,6 +185,9 @@ const isAuroraActive = (item) => normalize(item.url) === current.value; :class="{ 'sidebar__sublink--active': isAuroraActive(item) }" > {{ item.title }} + @@ -382,6 +385,18 @@ const isAuroraActive = (item) => normalize(item.url) === current.value; font-weight: 600; } +.sidebar__pro-badge { + display: flex; + align-items: center; + color: var(--aurora-text-muted); + flex-shrink: 0; +} +.sidebar__pro-badge .dashicons { + font-size: 13px; + width: 13px; + height: 13px; +} + /* --- Aurora branded nav (right under the native menu's own divider) ---- */ .sidebar__aurora { padding-bottom: 4px; diff --git a/app/vite.config.js b/app/vite.config.js index d051dbe..9b9d3d3 100644 --- a/app/vite.config.js +++ b/app/vite.config.js @@ -30,6 +30,7 @@ export default defineConfig({ 'database-explorer': fileURLToPath(new URL('./src/entries/database-explorer.js', import.meta.url)), plugins: fileURLToPath(new URL('./src/entries/plugins.js', import.meta.url)), comments: fileURLToPath(new URL('./src/entries/comments.js', import.meta.url)), + 'locked-feature': fileURLToPath(new URL('./src/entries/locked-feature.js', import.meta.url)), }, output: { entryFileNames: '[name].[hash].js', diff --git a/aurora-admin.php b/aurora-admin.php index e2618ed..797ce28 100644 --- a/aurora-admin.php +++ b/aurora-admin.php @@ -20,6 +20,7 @@ define("AURORA_ADMIN_URL", plugins_url("aurora-admin/")); require AURORA_ADMIN_PATH . "admin/src/App/AuroraAdmin.php"; require AURORA_ADMIN_PATH . "admin/src/Options/Settings.php"; +require AURORA_ADMIN_PATH . "admin/src/Licensing/License.php"; require AURORA_ADMIN_PATH . "admin/src/Utility/Assets.php"; require AURORA_ADMIN_PATH . "admin/src/Shell/MenuSerializer.php"; require AURORA_ADMIN_PATH . "admin/src/Shell/Shell.php";