# Addon-Module Integration: Design Decisions **Date**: December 26, 2025 **Status**: 🎯 Decision Document --- ## 1. Dynamic Categories (RECOMMENDED) ### ❌ Problem with Static Categories ```php // BAD: Empty categories if no modules use them public static function get_categories() { return [ 'shipping' => 'Shipping & Fulfillment', // Empty if no shipping modules! 'payments' => 'Payments & Checkout', // Empty if no payment modules! ]; } ``` ### ✅ Solution: Dynamic Category Generation ```php class ModuleRegistry { /** * Get categories dynamically from registered modules */ 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 (if exists), then alphabetically $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 */ 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 */ 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; } } ``` ### Benefits - ✅ No empty categories - ✅ Addons can define custom categories - ✅ Single registration point (module only) - ✅ Auto-sorted by predefined order --- ## 2. Module Settings URL Pattern (RECOMMENDED) ### ❌ Problem with Custom URLs ```php 'settings_url' => '/settings/shipping/biteship', // Conflict risk! 'settings_url' => '/marketing/newsletter', // Inconsistent! ``` ### ✅ Solution: Convention-Based Pattern #### Option A: Standardized Pattern (RECOMMENDED) ```php // Module registration - NO settings_url needed! $addons['biteship-shipping'] = [ 'id' => 'biteship-shipping', 'name' => 'Biteship Shipping', 'has_settings' => true, // Just a flag! ]; // Auto-generated URL pattern: // /settings/modules/{module_id} // Example: /settings/modules/biteship-shipping ``` #### Backend: Auto Route Registration ```php class ModuleRegistry { /** * Register module settings routes automatically */ public static function register_settings_routes() { $modules = self::get_all_modules(); foreach ($modules as $module) { if (empty($module['has_settings'])) continue; // Auto-register route: /settings/modules/{module_id} add_filter('woonoow/spa_routes', function($routes) use ($module) { $routes[] = [ 'path' => "/settings/modules/{$module['id']}", 'component_url' => $module['settings_component'] ?? null, 'title' => sprintf(__('%s Settings', 'woonoow'), $module['label']), ]; return $routes; }); } } } ``` #### Frontend: Automatic Navigation ```tsx // Modules.tsx - Gear icon auto-links {module.has_settings && module.enabled && ( )} ``` ### Benefits - ✅ No URL conflicts (enforced pattern) - ✅ Consistent navigation - ✅ Simpler addon registration - ✅ Auto-generated breadcrumbs --- ## 3. Form Builder vs Custom HTML (HYBRID APPROACH) ### ✅ Recommended: Provide Both Options #### Option A: Schema-Based Form Builder (For Simple Settings) ```php // Addon defines settings schema add_filter('woonoow/module_settings_schema', function($schemas) { $schemas['biteship-shipping'] = [ 'api_key' => [ 'type' => 'text', 'label' => 'API Key', 'description' => 'Your Biteship API key', 'required' => true, ], 'enable_tracking' => [ 'type' => 'toggle', 'label' => 'Enable Tracking', 'default' => true, ], 'default_courier' => [ 'type' => 'select', 'label' => 'Default Courier', 'options' => [ 'jne' => 'JNE', 'jnt' => 'J&T Express', 'sicepat' => 'SiCepat', ], ], ]; return $schemas; }); ``` **Auto-rendered form** - No React needed! #### Option B: Custom React Component (For Complex Settings) ```php // Addon provides custom React component add_filter('woonoow/addon_registry', function($addons) { $addons['biteship-shipping'] = [ 'id' => 'biteship-shipping', 'has_settings' => true, 'settings_component' => plugin_dir_url(__FILE__) . 'dist/Settings.js', ]; return $addons; }); ``` **Full control** - Custom React UI ### Implementation ```php class ModuleSettingsRenderer { /** * Render settings page */ public static function render($module_id) { $module = ModuleRegistry::get_module($module_id); // Option 1: Has custom component if (!empty($module['settings_component'])) { return self::render_custom_component($module); } // Option 2: Has schema - auto-generate form $schema = apply_filters('woonoow/module_settings_schema', []); if (isset($schema[$module_id])) { return self::render_schema_form($module_id, $schema[$module_id]); } // Option 3: No settings return ['error' => 'No settings available']; } } ``` ### Benefits - ✅ Simple addons use schema (no React needed) - ✅ Complex addons use custom components - ✅ Consistent data persistence for both - ✅ Gradual complexity curve --- ## 4. Settings Data Persistence (STANDARDIZED) ### ✅ Recommended: Unified Settings API #### Backend: Automatic Persistence ```php class ModuleSettingsController extends WP_REST_Controller { /** * GET /woonoow/v1/modules/{module_id}/settings */ public function get_settings($request) { $module_id = $request['module_id']; $settings = get_option("woonoow_module_{$module_id}_settings", []); // Apply defaults from schema $schema = apply_filters('woonoow/module_settings_schema', []); if (isset($schema[$module_id])) { $settings = wp_parse_args($settings, self::get_defaults($schema[$module_id])); } return rest_ensure_response($settings); } /** * POST /woonoow/v1/modules/{module_id}/settings */ public function update_settings($request) { $module_id = $request['module_id']; $new_settings = $request->get_json_params(); // Validate against schema $schema = apply_filters('woonoow/module_settings_schema', []); if (isset($schema[$module_id])) { $validated = self::validate_settings($new_settings, $schema[$module_id]); if (is_wp_error($validated)) { return $validated; } $new_settings = $validated; } // Save update_option("woonoow_module_{$module_id}_settings", $new_settings); // Allow addons to react do_action("woonoow/module_settings_updated/{$module_id}", $new_settings); return rest_ensure_response(['success' => true]); } } ``` #### Frontend: Unified Hook ```tsx // useModuleSettings.ts export function useModuleSettings(moduleId: string) { const queryClient = useQueryClient(); const { data: settings, isLoading } = useQuery({ queryKey: ['module-settings', moduleId], queryFn: async () => { const response = await api.get(`/modules/${moduleId}/settings`); return response; }, }); const updateSettings = useMutation({ mutationFn: async (newSettings: any) => { return api.post(`/modules/${moduleId}/settings`, newSettings); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['module-settings', moduleId] }); toast.success('Settings saved'); }, }); return { settings, isLoading, updateSettings }; } ``` #### Addon Usage ```tsx // Custom settings component export default function BiteshipSettings() { const { settings, updateSettings } = useModuleSettings('biteship-shipping'); return ( updateSettings.mutate({ api_key: e.target.value })} /> ); } ``` ### Benefits - ✅ Consistent storage pattern: `woonoow_module_{id}_settings` - ✅ Automatic validation (if schema provided) - ✅ React hook for easy access - ✅ Action hooks for addon logic --- ## 5. React Extension Pattern (DOCUMENTED) ### ✅ Solution: Window API + Build Externals #### WooNooW Core Exposes React ```typescript // admin-spa/src/main.tsx import React from 'react'; import ReactDOM from 'react-dom/client'; import { useQuery, useMutation } from '@tanstack/react-query'; // Expose for addons window.WooNooW = { React, ReactDOM, hooks: { useQuery, useMutation, useModuleSettings, // Our custom hook! }, components: { SettingsLayout, SettingsCard, Button, Input, Select, Switch, // ... all shadcn components }, utils: { api, toast, }, }; ``` #### Addon Development ```typescript // addon/src/Settings.tsx const { React, hooks, components, utils } = window.WooNooW; const { useModuleSettings } = hooks; const { SettingsLayout, SettingsCard, Input, Button } = components; const { toast } = utils; export default function BiteshipSettings() { const { settings, updateSettings } = useModuleSettings('biteship-shipping'); const [apiKey, setApiKey] = React.useState(settings?.api_key || ''); const handleSave = () => { updateSettings.mutate({ api_key: apiKey }); }; return React.createElement(SettingsLayout, { title: 'Biteship Settings' }, React.createElement(SettingsCard, null, React.createElement(Input, { label: 'API Key', value: apiKey, onChange: (e) => setApiKey(e.target.value), }), React.createElement(Button, { onClick: handleSave }, 'Save') ) ); } ``` #### With JSX (Build Required) ```tsx // addon/src/Settings.tsx const { React, hooks, components } = window.WooNooW; const { useModuleSettings } = hooks; const { SettingsLayout, SettingsCard, Input, Button } = components; export default function BiteshipSettings() { const { settings, updateSettings } = useModuleSettings('biteship-shipping'); return ( updateSettings.mutate({ api_key: e.target.value })} /> ); } ``` ```javascript // vite.config.js export default { build: { lib: { entry: 'src/Settings.tsx', formats: ['es'], }, rollupOptions: { external: ['react', 'react-dom'], output: { globals: { react: 'window.WooNooW.React', 'react-dom': 'window.WooNooW.ReactDOM', }, }, }, }, }; ``` ### Benefits - ✅ Addons don't bundle React (use ours) - ✅ Access to all WooNooW components - ✅ Consistent UI automatically - ✅ Type safety with TypeScript --- ## 6. Newsletter as Addon Example (RECOMMENDED) ### ✅ Yes, Refactor Newsletter as Built-in Addon #### Why This is Valuable 1. **Dogfooding** - We use our own addon system 2. **Example** - Best reference for addon developers 3. **Consistency** - Newsletter follows same pattern as external addons 4. **Testing** - Proves the system works #### Proposed Structure ``` includes/ Modules/ Newsletter/ NewsletterModule.php # Module registration NewsletterController.php # API endpoints (moved from Api/) NewsletterSettings.php # Settings schema admin-spa/src/modules/ Newsletter/ Settings.tsx # Settings page Subscribers.tsx # Subscribers page index.ts # Module exports ``` #### Registration Pattern ```php // includes/Modules/Newsletter/NewsletterModule.php class NewsletterModule { public static function register() { // Register as module add_filter('woonoow/builtin_modules', function($modules) { $modules['newsletter'] = [ 'id' => 'newsletter', 'label' => __('Newsletter', 'woonoow'), 'description' => __('Email newsletter subscriptions', 'woonoow'), 'category' => 'marketing', 'icon' => 'mail', 'default_enabled' => true, 'has_settings' => true, 'settings_component' => self::get_settings_url(), ]; return $modules; }); // Register routes (only if enabled) if (ModuleRegistry::is_enabled('newsletter')) { self::register_routes(); } } private static function register_routes() { // Settings route add_filter('woonoow/spa_routes', function($routes) { $routes[] = [ 'path' => '/settings/modules/newsletter', 'component_url' => plugins_url('admin-spa/dist/modules/Newsletter/Settings.js', WOONOOW_FILE), ]; return $routes; }); // Subscribers route add_filter('woonoow/spa_routes', function($routes) { $routes[] = [ 'path' => '/marketing/newsletter', 'component_url' => plugins_url('admin-spa/dist/modules/Newsletter/Subscribers.js', WOONOOW_FILE), ]; return $routes; }); } } ``` ### Benefits - ✅ Newsletter becomes reference implementation - ✅ Proves addon system works for complex modules - ✅ Shows best practices - ✅ Easier to maintain (follows pattern) --- ## Summary of Decisions | # | Question | Decision | Rationale | |---|----------|----------|-----------| | 1 | Categories | **Dynamic from modules** | No empty categories, single registration | | 2 | Settings URL | **Pattern: `/settings/modules/{id}`** | No conflicts, consistent, auto-generated | | 3 | Form Builder | **Hybrid: Schema + Custom** | Simple for basic, flexible for complex | | 4 | Data Persistence | **Unified API + Hook** | Consistent storage, easy access | | 5 | React Extension | **Window API + Externals** | No bundling, access to components | | 6 | Newsletter Refactor | **Yes, as example** | Dogfooding, reference implementation | --- ## Implementation Order ### Phase 1: Foundation 1. ✅ Dynamic category generation 2. ✅ Standardized settings URL pattern 3. ✅ Module settings API endpoints 4. ✅ `useModuleSettings` hook ### Phase 2: Form System 1. ✅ Schema-based form renderer 2. ✅ Custom component loader 3. ✅ Settings validation ### Phase 3: UI Enhancement 1. ✅ Search input on Modules page 2. ✅ Category filter pills 3. ✅ Gear icon with auto-routing ### Phase 4: Example 1. ✅ Refactor Newsletter as built-in addon 2. ✅ Document pattern 3. ✅ Create external addon example (Biteship) --- ## Next Steps **Ready to implement?** We have clear decisions on all 6 questions. Should we: 1. Start with Phase 1 (Foundation)? 2. Create the schema-based form system first? 3. Refactor Newsletter as proof-of-concept? **Your call!** All design decisions are documented and justified.