- 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
617 lines
17 KiB
Markdown
617 lines
17 KiB
Markdown
# 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 && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => navigate(`/settings/modules/${module.id}`)}
|
|
>
|
|
<Settings className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
```
|
|
|
|
### 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 (
|
|
<SettingsLayout title="Biteship Settings">
|
|
<SettingsCard>
|
|
<Input
|
|
label="API Key"
|
|
value={settings?.api_key || ''}
|
|
onChange={(e) => updateSettings.mutate({ api_key: e.target.value })}
|
|
/>
|
|
</SettingsCard>
|
|
</SettingsLayout>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 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 (
|
|
<SettingsLayout title="Biteship Settings">
|
|
<SettingsCard>
|
|
<Input
|
|
label="API Key"
|
|
value={settings?.api_key || ''}
|
|
onChange={(e) => updateSettings.mutate({ api_key: e.target.value })}
|
|
/>
|
|
</SettingsCard>
|
|
</SettingsLayout>
|
|
);
|
|
}
|
|
```
|
|
|
|
```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.
|