feat: Implement centralized module management system
- Add ModuleRegistry for managing built-in modules (newsletter, wishlist, affiliate, subscription, licensing) - Add ModulesController REST API for module enable/disable - Create Modules settings page with category grouping and toggle controls - Integrate module checks across admin-spa and customer-spa - Add useModules hook for both SPAs to check module status - Hide newsletter from footer builder when module disabled - Hide wishlist features when module disabled (product cards, account menu, wishlist page) - Protect wishlist API endpoints with module checks - Auto-update navigation tree when modules toggled - Clean up obsolete documentation files - Add comprehensive documentation: - MODULE_SYSTEM_IMPLEMENTATION.md - MODULE_INTEGRATION_SUMMARY.md - ADDON_MODULE_INTEGRATION.md (proposal) - ADDON_MODULE_DESIGN_DECISIONS.md (design doc) - FEATURE_ROADMAP.md - SHIPPING_INTEGRATION.md Module system provides: - Centralized enable/disable for all features - Automatic navigation updates - Frontend/backend integration - Foundation for addon-module unification
This commit is contained in:
167
includes/Api/ModulesController.php
Normal file
167
includes/Api/ModulesController.php
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
/**
|
||||
* Modules REST API Controller
|
||||
*
|
||||
* @package WooNooW\Api
|
||||
*/
|
||||
|
||||
namespace WooNooW\Api;
|
||||
|
||||
use WP_REST_Controller;
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_Error;
|
||||
use WooNooW\Core\ModuleRegistry;
|
||||
|
||||
class ModulesController extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* REST API namespace
|
||||
*/
|
||||
protected $namespace = 'woonoow/v1';
|
||||
|
||||
/**
|
||||
* REST API base
|
||||
*/
|
||||
protected $rest_base = 'modules';
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public function register_routes() {
|
||||
// GET /woonoow/v1/modules
|
||||
register_rest_route($this->namespace, '/' . $this->rest_base, [
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'get_modules'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
],
|
||||
]);
|
||||
|
||||
// POST /woonoow/v1/modules/toggle
|
||||
register_rest_route($this->namespace, '/' . $this->rest_base . '/toggle', [
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'toggle_module'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'module_id' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
],
|
||||
'enabled' => [
|
||||
'required' => true,
|
||||
'type' => 'boolean',
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// GET /woonoow/v1/modules/enabled (public endpoint for frontend)
|
||||
register_rest_route($this->namespace, '/' . $this->rest_base . '/enabled', [
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'get_enabled_modules'],
|
||||
'permission_callback' => '__return_true',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check permission
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function check_permission() {
|
||||
return current_user_can('manage_options');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all modules with status
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function get_modules($request) {
|
||||
$modules = ModuleRegistry::get_all_with_status();
|
||||
|
||||
// Group by category
|
||||
$grouped = [
|
||||
'marketing' => [],
|
||||
'customers' => [],
|
||||
'products' => [],
|
||||
];
|
||||
|
||||
foreach ($modules as $module) {
|
||||
$category = $module['category'];
|
||||
if (isset($grouped[$category])) {
|
||||
$grouped[$category][] = $module;
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_REST_Response([
|
||||
'modules' => $modules,
|
||||
'grouped' => $grouped,
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle module enabled/disabled
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function toggle_module($request) {
|
||||
$module_id = $request->get_param('module_id');
|
||||
$enabled = $request->get_param('enabled');
|
||||
|
||||
$modules = ModuleRegistry::get_all_modules();
|
||||
|
||||
if (!isset($modules[$module_id])) {
|
||||
return new WP_Error(
|
||||
'invalid_module',
|
||||
__('Invalid module ID', 'woonoow'),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
if ($enabled) {
|
||||
$result = ModuleRegistry::enable($module_id);
|
||||
} else {
|
||||
$result = ModuleRegistry::disable($module_id);
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'message' => $enabled
|
||||
? __('Module enabled successfully', 'woonoow')
|
||||
: __('Module disabled successfully', 'woonoow'),
|
||||
'module_id' => $module_id,
|
||||
'enabled' => $enabled,
|
||||
], 200);
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'toggle_failed',
|
||||
__('Failed to toggle module', 'woonoow'),
|
||||
['status' => 500]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled modules (public endpoint)
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function get_enabled_modules($request) {
|
||||
$enabled = ModuleRegistry::get_enabled_modules();
|
||||
|
||||
return new WP_REST_Response([
|
||||
'enabled' => $enabled,
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ use WooNooW\Api\ProductsController;
|
||||
use WooNooW\Api\CouponsController;
|
||||
use WooNooW\Api\CustomersController;
|
||||
use WooNooW\Api\NewsletterController;
|
||||
use WooNooW\Api\ModulesController;
|
||||
use WooNooW\Frontend\ShopController;
|
||||
use WooNooW\Frontend\CartController as FrontendCartController;
|
||||
use WooNooW\Frontend\AccountController;
|
||||
@@ -123,6 +124,10 @@ class Routes {
|
||||
// Newsletter controller
|
||||
NewsletterController::register_routes();
|
||||
|
||||
// Modules controller
|
||||
$modules_controller = new ModulesController();
|
||||
$modules_controller->register_routes();
|
||||
|
||||
// Frontend controllers (customer-facing)
|
||||
ShopController::register_routes();
|
||||
FrontendCartController::register_routes();
|
||||
|
||||
@@ -13,7 +13,7 @@ if ( ! defined('ABSPATH') ) exit;
|
||||
*/
|
||||
class NavigationRegistry {
|
||||
const NAV_OPTION = 'wnw_nav_tree';
|
||||
const NAV_VERSION = '1.0.7'; // Removed 'New Coupon' from submenu
|
||||
const NAV_VERSION = '1.0.8'; // Added Modules to Settings menu
|
||||
|
||||
/**
|
||||
* Initialize hooks
|
||||
@@ -105,7 +105,7 @@ class NavigationRegistry {
|
||||
* @return array Base navigation tree
|
||||
*/
|
||||
private static function get_base_tree(): array {
|
||||
return [
|
||||
$tree = [
|
||||
[
|
||||
'key' => 'dashboard',
|
||||
'label' => __('Dashboard', 'woonoow'),
|
||||
@@ -160,10 +160,7 @@ class NavigationRegistry {
|
||||
'label' => __('Marketing', 'woonoow'),
|
||||
'path' => '/marketing',
|
||||
'icon' => 'mail',
|
||||
'children' => [
|
||||
['label' => __('Newsletter', 'woonoow'), 'mode' => 'spa', 'path' => '/marketing/newsletter'],
|
||||
['label' => __('Coupons', 'woonoow'), 'mode' => 'spa', 'path' => '/coupons'],
|
||||
],
|
||||
'children' => self::get_marketing_children(),
|
||||
],
|
||||
[
|
||||
'key' => 'appearance',
|
||||
@@ -190,6 +187,27 @@ class NavigationRegistry {
|
||||
'children' => self::get_settings_children(),
|
||||
],
|
||||
];
|
||||
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get marketing submenu children
|
||||
*
|
||||
* @return array Marketing submenu items
|
||||
*/
|
||||
private static function get_marketing_children(): array {
|
||||
$children = [];
|
||||
|
||||
// Newsletter - only if module enabled
|
||||
if (\WooNooW\Core\ModuleRegistry::is_enabled('newsletter')) {
|
||||
$children[] = ['label' => __('Newsletter', 'woonoow'), 'mode' => 'spa', 'path' => '/marketing/newsletter'];
|
||||
}
|
||||
|
||||
// Coupons - always available
|
||||
$children[] = ['label' => __('Coupons', 'woonoow'), 'mode' => 'spa', 'path' => '/coupons'];
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,6 +226,7 @@ class NavigationRegistry {
|
||||
['label' => __('Tax', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/tax'],
|
||||
['label' => __('Customers', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/customers'],
|
||||
['label' => __('Notifications', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/notifications'],
|
||||
['label' => __('Modules', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/modules'],
|
||||
['label' => __('Developer', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/developer'],
|
||||
];
|
||||
|
||||
|
||||
223
includes/Core/ModuleRegistry.php
Normal file
223
includes/Core/ModuleRegistry.php
Normal file
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
/**
|
||||
* Module Registry
|
||||
*
|
||||
* Central registry for managing WooNooW modules (features).
|
||||
* Allows enabling/disabling modules to improve performance and reduce clutter.
|
||||
*
|
||||
* @package WooNooW\Core
|
||||
*/
|
||||
|
||||
namespace WooNooW\Core;
|
||||
|
||||
class ModuleRegistry {
|
||||
|
||||
/**
|
||||
* Get all registered modules
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_all_modules() {
|
||||
$modules = [
|
||||
'newsletter' => [
|
||||
'id' => 'newsletter',
|
||||
'label' => __('Newsletter & Campaigns', 'woonoow'),
|
||||
'description' => __('Email newsletter subscription and campaign management', 'woonoow'),
|
||||
'category' => 'marketing',
|
||||
'icon' => 'mail',
|
||||
'default_enabled' => true,
|
||||
'features' => [
|
||||
__('Subscriber management', 'woonoow'),
|
||||
__('Email campaigns', 'woonoow'),
|
||||
__('Campaign scheduling', 'woonoow'),
|
||||
],
|
||||
],
|
||||
'wishlist' => [
|
||||
'id' => 'wishlist',
|
||||
'label' => __('Customer Wishlist', 'woonoow'),
|
||||
'description' => __('Allow customers to save products for later', 'woonoow'),
|
||||
'category' => 'customers',
|
||||
'icon' => 'heart',
|
||||
'default_enabled' => true,
|
||||
'features' => [
|
||||
__('Save products to wishlist', 'woonoow'),
|
||||
__('Wishlist page', 'woonoow'),
|
||||
__('Share wishlist', 'woonoow'),
|
||||
],
|
||||
],
|
||||
'affiliate' => [
|
||||
'id' => 'affiliate',
|
||||
'label' => __('Affiliate Program', 'woonoow'),
|
||||
'description' => __('Referral tracking and commission management', 'woonoow'),
|
||||
'category' => 'marketing',
|
||||
'icon' => 'users',
|
||||
'default_enabled' => false,
|
||||
'features' => [
|
||||
__('Referral tracking', 'woonoow'),
|
||||
__('Commission management', 'woonoow'),
|
||||
__('Affiliate dashboard', 'woonoow'),
|
||||
__('Payout system', 'woonoow'),
|
||||
],
|
||||
],
|
||||
'subscription' => [
|
||||
'id' => 'subscription',
|
||||
'label' => __('Product Subscriptions', 'woonoow'),
|
||||
'description' => __('Recurring product subscriptions with flexible billing', 'woonoow'),
|
||||
'category' => 'products',
|
||||
'icon' => 'refresh-cw',
|
||||
'default_enabled' => false,
|
||||
'features' => [
|
||||
__('Recurring billing', 'woonoow'),
|
||||
__('Subscription management', 'woonoow'),
|
||||
__('Automatic renewals', 'woonoow'),
|
||||
__('Trial periods', 'woonoow'),
|
||||
],
|
||||
],
|
||||
'licensing' => [
|
||||
'id' => 'licensing',
|
||||
'label' => __('Software Licensing', 'woonoow'),
|
||||
'description' => __('License key generation and validation for digital products', 'woonoow'),
|
||||
'category' => 'products',
|
||||
'icon' => 'key',
|
||||
'default_enabled' => false,
|
||||
'features' => [
|
||||
__('License key generation', 'woonoow'),
|
||||
__('Activation management', 'woonoow'),
|
||||
__('Validation API', 'woonoow'),
|
||||
__('Expiry management', 'woonoow'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return apply_filters('woonoow/modules/registry', $modules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled modules
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_enabled_modules() {
|
||||
$enabled = get_option('woonoow_enabled_modules', null);
|
||||
|
||||
// First time - use defaults
|
||||
if ($enabled === null) {
|
||||
$modules = self::get_all_modules();
|
||||
$enabled = [];
|
||||
foreach ($modules as $module) {
|
||||
if ($module['default_enabled']) {
|
||||
$enabled[] = $module['id'];
|
||||
}
|
||||
}
|
||||
update_option('woonoow_enabled_modules', $enabled);
|
||||
}
|
||||
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a module is enabled
|
||||
*
|
||||
* @param string $module_id
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_enabled($module_id) {
|
||||
$enabled = self::get_enabled_modules();
|
||||
return in_array($module_id, $enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a module
|
||||
*
|
||||
* @param string $module_id
|
||||
* @return bool
|
||||
*/
|
||||
public static function enable($module_id) {
|
||||
$modules = self::get_all_modules();
|
||||
|
||||
if (!isset($modules[$module_id])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$enabled = self::get_enabled_modules();
|
||||
|
||||
if (!in_array($module_id, $enabled)) {
|
||||
$enabled[] = $module_id;
|
||||
update_option('woonoow_enabled_modules', $enabled);
|
||||
|
||||
// Clear navigation cache when module is toggled
|
||||
if (class_exists('\WooNooW\Compat\NavigationRegistry')) {
|
||||
\WooNooW\Compat\NavigationRegistry::flush();
|
||||
}
|
||||
|
||||
do_action('woonoow/module/enabled', $module_id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a module
|
||||
*
|
||||
* @param string $module_id
|
||||
* @return bool
|
||||
*/
|
||||
public static function disable($module_id) {
|
||||
$enabled = self::get_enabled_modules();
|
||||
|
||||
if (in_array($module_id, $enabled)) {
|
||||
$enabled = array_diff($enabled, [$module_id]);
|
||||
update_option('woonoow_enabled_modules', array_values($enabled));
|
||||
|
||||
// Clear navigation cache when module is toggled
|
||||
if (class_exists('\WooNooW\Compat\NavigationRegistry')) {
|
||||
\WooNooW\Compat\NavigationRegistry::flush();
|
||||
}
|
||||
|
||||
do_action('woonoow/module/disabled', $module_id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get modules by category
|
||||
*
|
||||
* @param string $category
|
||||
* @return array
|
||||
*/
|
||||
public static function get_by_category($category) {
|
||||
$modules = self::get_all_modules();
|
||||
$enabled = self::get_enabled_modules();
|
||||
|
||||
$result = [];
|
||||
foreach ($modules as $module) {
|
||||
if ($module['category'] === $category) {
|
||||
$module['enabled'] = in_array($module['id'], $enabled);
|
||||
$result[] = $module;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all modules with enabled status
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_all_with_status() {
|
||||
$modules = self::get_all_modules();
|
||||
$enabled = self::get_enabled_modules();
|
||||
|
||||
foreach ($modules as $id => $module) {
|
||||
$modules[$id]['enabled'] = in_array($id, $enabled);
|
||||
}
|
||||
|
||||
return $modules;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace WooNooW\Frontend;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_Error;
|
||||
use WooNooW\Core\ModuleRegistry;
|
||||
|
||||
class WishlistController {
|
||||
|
||||
@@ -60,6 +61,10 @@ class WishlistController {
|
||||
* Get wishlist items with product details
|
||||
*/
|
||||
public static function get_wishlist(WP_REST_Request $request) {
|
||||
if (!ModuleRegistry::is_enabled('wishlist')) {
|
||||
return new WP_Error('module_disabled', __('Wishlist module is disabled', 'woonoow'), ['status' => 403]);
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$wishlist = get_user_meta($user_id, 'woonoow_wishlist', true);
|
||||
|
||||
@@ -98,6 +103,10 @@ class WishlistController {
|
||||
* Add product to wishlist
|
||||
*/
|
||||
public static function add_to_wishlist(WP_REST_Request $request) {
|
||||
if (!ModuleRegistry::is_enabled('wishlist')) {
|
||||
return new WP_Error('module_disabled', __('Wishlist module is disabled', 'woonoow'), ['status' => 403]);
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$product_id = $request->get_param('product_id');
|
||||
|
||||
@@ -137,6 +146,10 @@ class WishlistController {
|
||||
* Remove product from wishlist
|
||||
*/
|
||||
public static function remove_from_wishlist(WP_REST_Request $request) {
|
||||
if (!ModuleRegistry::is_enabled('wishlist')) {
|
||||
return new WP_Error('module_disabled', __('Wishlist module is disabled', 'woonoow'), ['status' => 403]);
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$product_id = (int) $request->get_param('product_id');
|
||||
|
||||
@@ -165,6 +178,10 @@ class WishlistController {
|
||||
* Clear entire wishlist
|
||||
*/
|
||||
public static function clear_wishlist(WP_REST_Request $request) {
|
||||
if (!ModuleRegistry::is_enabled('wishlist')) {
|
||||
return new WP_Error('module_disabled', __('Wishlist module is disabled', 'woonoow'), ['status' => 403]);
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
delete_user_meta($user_id, 'woonoow_wishlist');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user