Files
WooNooW/includes/Core/ModuleRegistry.php
Dwindi Ramadhana c6cef97ef8 feat: Implement Phase 2, 3, 4 - Module Settings System with Schema Forms and Addon API
Phase 2: Schema-Based Form System
- Add ModuleSettingsController with GET/POST/schema endpoints
- Create SchemaField component supporting 8 field types (text, textarea, email, url, number, toggle, checkbox, select)
- Create SchemaForm component for automatic form generation from schema
- Add ModuleSettings page with dynamic routing (/settings/modules/:moduleId)
- Add useModuleSettings React hook for settings management
- Implement NewsletterSettings as example with 8 configurable fields
- Add has_settings flag to module registry
- Settings stored as woonoow_module_{module_id}_settings

Phase 3: Advanced Features
- Create windowAPI.ts exposing React, hooks, components, icons, utils to addons via window.WooNooW
- Add DynamicComponentLoader for loading external React components
- Create TypeScript definitions (woonoow-addon.d.ts) for addon developers
- Initialize Window API in App.tsx on mount
- Enable custom React components for addon settings pages

Phase 4: Production Polish & Example
- Create complete Biteship addon example demonstrating both approaches:
  * Schema-based settings (no build required)
  * Custom React component (with build)
- Add comprehensive README with installation and testing guide
- Include package.json with esbuild configuration
- Demonstrate window.WooNooW API usage in custom component

Bug Fixes:
- Fix footer newsletter form visibility (remove redundant module check)
- Fix footer contact_data and social_links not saving (parameter name mismatch: snake_case vs camelCase)
- Fix useModules hook returning undefined (remove .data wrapper, add fallback)
- Add optional chaining to footer settings rendering
- Fix TypeScript errors in woonoow-addon.d.ts (use any for external types)

Files Added (15):
- includes/Api/ModuleSettingsController.php
- includes/Modules/NewsletterSettings.php
- admin-spa/src/components/forms/SchemaField.tsx
- admin-spa/src/components/forms/SchemaForm.tsx
- admin-spa/src/routes/Settings/ModuleSettings.tsx
- admin-spa/src/hooks/useModuleSettings.ts
- admin-spa/src/lib/windowAPI.ts
- admin-spa/src/components/DynamicComponentLoader.tsx
- types/woonoow-addon.d.ts
- examples/biteship-addon/biteship-addon.php
- examples/biteship-addon/src/Settings.jsx
- examples/biteship-addon/package.json
- examples/biteship-addon/README.md
- PHASE_2_3_4_SUMMARY.md

Files Modified (11):
- admin-spa/src/App.tsx
- admin-spa/src/hooks/useModules.ts
- admin-spa/src/routes/Appearance/Footer.tsx
- admin-spa/src/routes/Settings/Modules.tsx
- customer-spa/src/hooks/useModules.ts
- customer-spa/src/layouts/BaseLayout.tsx
- customer-spa/src/components/NewsletterForm.tsx
- includes/Api/Routes.php
- includes/Api/ModulesController.php
- includes/Core/ModuleRegistry.php
- woonoow.php

API Endpoints Added:
- GET /woonoow/v1/modules/{module_id}/settings
- POST /woonoow/v1/modules/{module_id}/settings
- GET /woonoow/v1/modules/{module_id}/schema

For Addon Developers:
- Schema-based: Define settings via woonoow/module_settings_schema filter
- Custom React: Build component using window.WooNooW API, externalize react/react-dom
- Both approaches use same storage and retrieval methods
- TypeScript definitions provided for type safety
- Complete working example (Biteship) included
2025-12-26 21:16:06 +07:00

336 lines
11 KiB
PHP

<?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 built-in modules
*
* @return array
*/
private static function get_builtin_modules() {
$modules = [
'newsletter' => [
'id' => 'newsletter',
'label' => __('Newsletter & Campaigns', 'woonoow'),
'description' => __('Email newsletter subscription and campaign management', 'woonoow'),
'category' => 'marketing',
'icon' => 'mail',
'default_enabled' => true,
'has_settings' => 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 $modules;
}
/**
* Get addon modules from AddonRegistry
*
* @return array
*/
private static function get_addon_modules() {
$addons = apply_filters('woonoow/addon_registry', []);
$modules = [];
foreach ($addons as $addon_id => $addon) {
$modules[$addon_id] = [
'id' => $addon_id,
'label' => $addon['name'] ?? ucfirst($addon_id),
'description' => $addon['description'] ?? '',
'category' => $addon['category'] ?? 'other',
'icon' => $addon['icon'] ?? 'puzzle',
'default_enabled' => false,
'features' => $addon['features'] ?? [],
'is_addon' => true,
'version' => $addon['version'] ?? '1.0.0',
'author' => $addon['author'] ?? '',
'has_settings' => !empty($addon['has_settings']),
'settings_component' => $addon['settings_component'] ?? null,
];
}
return $modules;
}
/**
* Get all modules (built-in + addons)
*
* @return array
*/
public static function get_all_modules() {
$builtin = self::get_builtin_modules();
$addons = self::get_addon_modules();
return array_merge($builtin, $addons);
}
/**
* Get categories dynamically from registered modules
*
* @return array Associative array of category_id => label
*/
public static function get_categories() {
$all_modules = self::get_all_modules();
$categories = [];
// Extract unique categories from modules
foreach ($all_modules as $module) {
$cat = $module['category'] ?? 'other';
if (!isset($categories[$cat])) {
$categories[$cat] = self::get_category_label($cat);
}
}
// Sort by predefined order
$order = ['marketing', 'customers', 'products', 'shipping', 'payments', 'analytics', 'other'];
uksort($categories, function($a, $b) use ($order) {
$pos_a = array_search($a, $order);
$pos_b = array_search($b, $order);
if ($pos_a === false) $pos_a = 999;
if ($pos_b === false) $pos_b = 999;
return $pos_a - $pos_b;
});
return $categories;
}
/**
* Get human-readable label for category
*
* @param string $category Category ID
* @return string
*/
private static function get_category_label($category) {
$labels = [
'marketing' => __('Marketing & Sales', 'woonoow'),
'customers' => __('Customer Experience', 'woonoow'),
'products' => __('Products & Inventory', 'woonoow'),
'shipping' => __('Shipping & Fulfillment', 'woonoow'),
'payments' => __('Payments & Checkout', 'woonoow'),
'analytics' => __('Analytics & Reports', 'woonoow'),
'other' => __('Other Extensions', 'woonoow'),
];
return $labels[$category] ?? ucfirst($category);
}
/**
* Group modules by category
*
* @return array
*/
public static function get_grouped_modules() {
$all_modules = self::get_all_modules();
$grouped = [];
foreach ($all_modules as $module) {
$cat = $module['category'] ?? 'other';
if (!isset($grouped[$cat])) {
$grouped[$cat] = [];
}
$grouped[$cat][] = $module;
}
return $grouped;
}
/**
* 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;
}
}