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:
Dwindi Ramadhana
2025-12-26 19:19:49 +07:00
parent 0b2c8a56d6
commit 07020bc0dd
59 changed files with 3891 additions and 12132 deletions

View 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);
}
}

View File

@@ -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();

View File

@@ -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'],
];

View 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;
}
}

View File

@@ -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');