Added comprehensive documentation: 1. ADDON_HOOK_SYSTEM.md - WordPress-style hook system for React - Zero coupling between core and addons - Addons register via hooks (no hardcoding) - Type-safe filter/action system 2. BITESHIP_ADDON_SPEC.md (partial) - Plugin structure and architecture - Database schema for Indonesian addresses - WooCommerce shipping method integration - REST API endpoints - React components specification Key Insight: ✅ Hook system = Universal, no addon-specific code ❌ Hardcoding = Breaks if addon not installed Next: Verify shipping settings work correctly
580 lines
15 KiB
Markdown
580 lines
15 KiB
Markdown
# WooNooW Addon Hook System
|
|
|
|
## Problem Statement
|
|
|
|
**Question:** How can WooNooW SPA support addons without hardcoding specific components?
|
|
|
|
**Example of WRONG approach:**
|
|
```typescript
|
|
// ❌ This is hardcoding - breaks if addon doesn't exist
|
|
import { SubdistrictSelector } from 'woonoow-indonesia-shipping';
|
|
|
|
<OrderForm>
|
|
<SubdistrictSelector /> {/* ❌ Error if plugin not installed */}
|
|
</OrderForm>
|
|
```
|
|
|
|
**This is "supporting specific 3rd party addons" - exactly what we want to AVOID!**
|
|
|
|
---
|
|
|
|
## **The Solution: WordPress-Style Hook System in React**
|
|
|
|
### **Architecture Overview**
|
|
|
|
```
|
|
WooNooW Core (Base):
|
|
- Provides hook points
|
|
- Renders whatever addons register
|
|
- No knowledge of specific addons
|
|
|
|
Addon Plugins:
|
|
- Register components via hooks
|
|
- Only loaded if plugin is active
|
|
- Self-contained functionality
|
|
```
|
|
|
|
---
|
|
|
|
## **Implementation**
|
|
|
|
### **Step 1: Create Hook System in WooNooW Core**
|
|
|
|
```typescript
|
|
// admin-spa/src/lib/hooks.ts
|
|
|
|
type HookCallback = (...args: any[]) => any;
|
|
|
|
class HookSystem {
|
|
private filters: Map<string, HookCallback[]> = new Map();
|
|
private actions: Map<string, HookCallback[]> = new Map();
|
|
|
|
/**
|
|
* Add a filter hook
|
|
* Similar to WordPress add_filter()
|
|
*/
|
|
addFilter(hookName: string, callback: HookCallback, priority: number = 10) {
|
|
if (!this.filters.has(hookName)) {
|
|
this.filters.set(hookName, []);
|
|
}
|
|
|
|
const hooks = this.filters.get(hookName)!;
|
|
hooks.push({ callback, priority });
|
|
hooks.sort((a, b) => a.priority - b.priority);
|
|
}
|
|
|
|
/**
|
|
* Apply filters
|
|
* Similar to WordPress apply_filters()
|
|
*/
|
|
applyFilters(hookName: string, value: any, ...args: any[]): any {
|
|
const hooks = this.filters.get(hookName) || [];
|
|
|
|
return hooks.reduce((currentValue, { callback }) => {
|
|
return callback(currentValue, ...args);
|
|
}, value);
|
|
}
|
|
|
|
/**
|
|
* Add an action hook
|
|
* Similar to WordPress add_action()
|
|
*/
|
|
addAction(hookName: string, callback: HookCallback, priority: number = 10) {
|
|
if (!this.actions.has(hookName)) {
|
|
this.actions.set(hookName, []);
|
|
}
|
|
|
|
const hooks = this.actions.get(hookName)!;
|
|
hooks.push({ callback, priority });
|
|
hooks.sort((a, b) => a.priority - b.priority);
|
|
}
|
|
|
|
/**
|
|
* Do action
|
|
* Similar to WordPress do_action()
|
|
*/
|
|
doAction(hookName: string, ...args: any[]) {
|
|
const hooks = this.actions.get(hookName) || [];
|
|
hooks.forEach(({ callback }) => callback(...args));
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const hooks = new HookSystem();
|
|
|
|
// Export helper functions
|
|
export const addFilter = hooks.addFilter.bind(hooks);
|
|
export const applyFilters = hooks.applyFilters.bind(hooks);
|
|
export const addAction = hooks.addAction.bind(hooks);
|
|
export const doAction = hooks.doAction.bind(hooks);
|
|
```
|
|
|
|
### **Step 2: Add Hook Points in OrderForm.tsx**
|
|
|
|
```typescript
|
|
// admin-spa/src/routes/Orders/OrderForm.tsx
|
|
import { applyFilters, doAction } from '@/lib/hooks';
|
|
|
|
export function OrderForm() {
|
|
const [formData, setFormData] = useState(initialData);
|
|
|
|
// Hook: Allow addons to modify form data
|
|
const processedFormData = applyFilters('woonoow_order_form_data', formData);
|
|
|
|
// Hook: Allow addons to add validation
|
|
const validateForm = () => {
|
|
let errors = {};
|
|
|
|
// Core validation
|
|
if (!formData.customer_id) {
|
|
errors.customer_id = 'Customer is required';
|
|
}
|
|
|
|
// Hook: Let addons add their validation
|
|
errors = applyFilters('woonoow_order_form_validation', errors, formData);
|
|
|
|
return errors;
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit}>
|
|
{/* Customer Section */}
|
|
<CustomerSection data={formData.customer} onChange={handleCustomerChange} />
|
|
|
|
{/* Billing Address */}
|
|
<AddressSection
|
|
type="billing"
|
|
data={formData.billing}
|
|
onChange={handleBillingChange}
|
|
/>
|
|
|
|
{/* Hook: Allow addons to inject fields after billing */}
|
|
{applyFilters('woonoow_order_form_after_billing', null, formData, setFormData)}
|
|
|
|
{/* Shipping Address */}
|
|
<AddressSection
|
|
type="shipping"
|
|
data={formData.shipping}
|
|
onChange={handleShippingChange}
|
|
/>
|
|
|
|
{/* Hook: Allow addons to inject fields after shipping */}
|
|
{applyFilters('woonoow_order_form_after_shipping', null, formData, setFormData)}
|
|
|
|
{/* Shipping Method Selection */}
|
|
<ShippingMethodSection>
|
|
{/* Core shipping method selector */}
|
|
<Select
|
|
label="Shipping Method"
|
|
options={shippingMethods}
|
|
value={formData.shipping_method}
|
|
onChange={(value) => setFormData({...formData, shipping_method: value})}
|
|
/>
|
|
|
|
{/* Hook: Allow addons to add custom shipping fields */}
|
|
{applyFilters('woonoow_order_form_shipping_fields', null, formData, setFormData)}
|
|
</ShippingMethodSection>
|
|
|
|
{/* Products */}
|
|
<ProductsSection data={formData.line_items} onChange={handleProductsChange} />
|
|
|
|
{/* Hook: Allow addons to add custom sections */}
|
|
{applyFilters('woonoow_order_form_custom_sections', null, formData, setFormData)}
|
|
|
|
{/* Submit */}
|
|
<Button type="submit">Create Order</Button>
|
|
</form>
|
|
);
|
|
}
|
|
```
|
|
|
|
### **Step 3: Addon Registration System**
|
|
|
|
```typescript
|
|
// admin-spa/src/lib/addon-loader.ts
|
|
|
|
interface AddonConfig {
|
|
id: string;
|
|
name: string;
|
|
version: string;
|
|
init: () => void;
|
|
}
|
|
|
|
class AddonLoader {
|
|
private addons: Map<string, AddonConfig> = new Map();
|
|
|
|
/**
|
|
* Register an addon
|
|
*/
|
|
register(config: AddonConfig) {
|
|
if (this.addons.has(config.id)) {
|
|
console.warn(`Addon ${config.id} is already registered`);
|
|
return;
|
|
}
|
|
|
|
this.addons.set(config.id, config);
|
|
|
|
// Initialize the addon
|
|
config.init();
|
|
|
|
console.log(`✅ Addon registered: ${config.name} v${config.version}`);
|
|
}
|
|
|
|
/**
|
|
* Check if addon is registered
|
|
*/
|
|
isRegistered(addonId: string): boolean {
|
|
return this.addons.has(addonId);
|
|
}
|
|
|
|
/**
|
|
* Get all registered addons
|
|
*/
|
|
getAll(): AddonConfig[] {
|
|
return Array.from(this.addons.values());
|
|
}
|
|
}
|
|
|
|
export const addonLoader = new AddonLoader();
|
|
```
|
|
|
|
### **Step 4: Load Addons from Backend**
|
|
|
|
```php
|
|
// includes/Core/AddonRegistry.php
|
|
|
|
namespace WooNooW\Core;
|
|
|
|
class AddonRegistry {
|
|
private static $addons = array();
|
|
|
|
/**
|
|
* Register an addon
|
|
*/
|
|
public static function register($addon_id, $addon_config) {
|
|
self::$addons[$addon_id] = $addon_config;
|
|
}
|
|
|
|
/**
|
|
* Get all registered addons
|
|
*/
|
|
public static function get_all() {
|
|
return self::$addons;
|
|
}
|
|
|
|
/**
|
|
* Enqueue addon scripts
|
|
*/
|
|
public static function enqueue_addon_scripts() {
|
|
foreach (self::$addons as $addon_id => $config) {
|
|
if (isset($config['script_url'])) {
|
|
wp_enqueue_script(
|
|
'woonoow-addon-' . $addon_id,
|
|
$config['script_url'],
|
|
array('woonoow-admin-spa'),
|
|
$config['version'],
|
|
true
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pass addon data to frontend
|
|
*/
|
|
public static function get_addon_data() {
|
|
return array(
|
|
'addons' => self::$addons,
|
|
'active_addons' => array_keys(self::$addons)
|
|
);
|
|
}
|
|
}
|
|
|
|
// Hook to enqueue addon scripts
|
|
add_action('admin_enqueue_scripts', array('WooNooW\Core\AddonRegistry', 'enqueue_addon_scripts'));
|
|
```
|
|
|
|
```php
|
|
// includes/Core/Bootstrap.php
|
|
|
|
// Add addon data to WNW_CONFIG
|
|
add_filter('woonoow_admin_config', function($config) {
|
|
$config['addons'] = \WooNooW\Core\AddonRegistry::get_addon_data();
|
|
return $config;
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## **Example: Indonesia Shipping Addon**
|
|
|
|
### **Addon Plugin Structure**
|
|
|
|
```
|
|
woonoow-indonesia-shipping/
|
|
├── woonoow-indonesia-shipping.php
|
|
├── includes/
|
|
│ └── class-addon-integration.php
|
|
└── admin-spa/
|
|
├── src/
|
|
│ ├── components/
|
|
│ │ ├── SubdistrictSelector.tsx
|
|
│ │ └── CourierSelector.tsx
|
|
│ └── index.ts
|
|
└── dist/
|
|
└── addon.js (built file)
|
|
```
|
|
|
|
### **Addon Main File**
|
|
|
|
```php
|
|
<?php
|
|
/**
|
|
* Plugin Name: WooNooW Indonesia Shipping
|
|
* Description: Indonesian shipping integration for WooNooW
|
|
* Version: 1.0.0
|
|
*/
|
|
|
|
// Register with WooNooW
|
|
add_action('woonoow_loaded', function() {
|
|
\WooNooW\Core\AddonRegistry::register('indonesia-shipping', array(
|
|
'name' => 'Indonesia Shipping',
|
|
'version' => '1.0.0',
|
|
'script_url' => plugin_dir_url(__FILE__) . 'admin-spa/dist/addon.js',
|
|
'has_settings' => true,
|
|
'settings_url' => admin_url('admin.php?page=woonoow-indonesia-shipping')
|
|
));
|
|
});
|
|
|
|
// Add REST API endpoints
|
|
add_action('rest_api_init', function() {
|
|
register_rest_route('woonoow/v1', '/indonesia-shipping/provinces', array(
|
|
'methods' => 'GET',
|
|
'callback' => 'get_provinces',
|
|
'permission_callback' => function() {
|
|
return current_user_can('manage_woocommerce');
|
|
}
|
|
));
|
|
|
|
// ... more endpoints
|
|
});
|
|
```
|
|
|
|
### **Addon Frontend Integration**
|
|
|
|
```typescript
|
|
// woonoow-indonesia-shipping/admin-spa/src/index.ts
|
|
|
|
import { addonLoader, addFilter } from '@woonoow/hooks';
|
|
import { SubdistrictSelector } from './components/SubdistrictSelector';
|
|
import { CourierSelector } from './components/CourierSelector';
|
|
|
|
// Register addon
|
|
addonLoader.register({
|
|
id: 'indonesia-shipping',
|
|
name: 'Indonesia Shipping',
|
|
version: '1.0.0',
|
|
init: () => {
|
|
console.log('🇮🇩 Indonesia Shipping addon loaded');
|
|
|
|
// Hook: Add subdistrict field after shipping address
|
|
addFilter('woonoow_order_form_after_shipping', (content, formData, setFormData) => {
|
|
return (
|
|
<>
|
|
{content}
|
|
<SubdistrictSelector
|
|
value={formData.shipping?.subdistrict_id}
|
|
onChange={(subdistrictId) => {
|
|
setFormData({
|
|
...formData,
|
|
shipping: {
|
|
...formData.shipping,
|
|
subdistrict_id: subdistrictId
|
|
}
|
|
});
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
});
|
|
|
|
// Hook: Add courier selector in shipping section
|
|
addFilter('woonoow_order_form_shipping_fields', (content, formData, setFormData) => {
|
|
// Only show if subdistrict is selected
|
|
if (!formData.shipping?.subdistrict_id) {
|
|
return content;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{content}
|
|
<CourierSelector
|
|
originSubdistrictId={formData.origin_subdistrict_id}
|
|
destinationSubdistrictId={formData.shipping.subdistrict_id}
|
|
weight={calculateTotalWeight(formData.line_items)}
|
|
onSelect={(courier) => {
|
|
setFormData({
|
|
...formData,
|
|
shipping_method: courier.id,
|
|
shipping_cost: courier.cost
|
|
});
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
});
|
|
|
|
// Hook: Add validation
|
|
addFilter('woonoow_order_form_validation', (errors, formData) => {
|
|
if (!formData.shipping?.subdistrict_id) {
|
|
errors.subdistrict = 'Subdistrict is required for Indonesian shipping';
|
|
}
|
|
return errors;
|
|
});
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## **How This Works**
|
|
|
|
### **Scenario 1: Addon is Installed**
|
|
|
|
1. WordPress loads `woonoow-indonesia-shipping.php`
|
|
2. Addon registers with `AddonRegistry`
|
|
3. WooNooW enqueues addon script (`addon.js`)
|
|
4. Addon script runs `addonLoader.register()`
|
|
5. Addon hooks are registered
|
|
6. OrderForm renders → hooks fire → addon components appear
|
|
|
|
**Result:** ✅ Subdistrict selector appears in order form
|
|
|
|
### **Scenario 2: Addon is NOT Installed**
|
|
|
|
1. No addon plugin loaded
|
|
2. No addon script enqueued
|
|
3. No hooks registered
|
|
4. OrderForm renders → hooks fire → return `null`
|
|
|
|
**Result:** ✅ No error, form works normally without addon fields
|
|
|
|
---
|
|
|
|
## **Key Differences from Hardcoding**
|
|
|
|
### ❌ **Hardcoding (WRONG)**
|
|
```typescript
|
|
// This breaks if addon doesn't exist
|
|
import { SubdistrictSelector } from 'woonoow-indonesia-shipping';
|
|
|
|
<OrderForm>
|
|
<SubdistrictSelector /> {/* ❌ Import error if plugin not installed */}
|
|
</OrderForm>
|
|
```
|
|
|
|
### ✅ **Hook System (CORRECT)**
|
|
```typescript
|
|
// This works whether addon exists or not
|
|
{applyFilters('woonoow_order_form_after_shipping', null, formData, setFormData)}
|
|
|
|
// If addon exists: Returns <SubdistrictSelector />
|
|
// If addon doesn't exist: Returns null
|
|
// No import, no error!
|
|
```
|
|
|
|
---
|
|
|
|
## **Benefits of Hook System**
|
|
|
|
### 1. **Zero Coupling**
|
|
- WooNooW Core has no knowledge of specific addons
|
|
- Addons can be installed/uninstalled without breaking core
|
|
- No hardcoded dependencies
|
|
|
|
### 2. **Extensibility**
|
|
- Any developer can create addons
|
|
- Multiple addons can hook into same points
|
|
- Addons can interact with each other
|
|
|
|
### 3. **WordPress-Like**
|
|
- Familiar pattern for WordPress developers
|
|
- Easy to understand and use
|
|
- Well-tested architecture
|
|
|
|
### 4. **Type Safety** (with TypeScript)
|
|
```typescript
|
|
// Define hook types
|
|
interface OrderFormData {
|
|
customer_id: number;
|
|
billing: Address;
|
|
shipping: Address & { subdistrict_id?: string };
|
|
line_items: LineItem[];
|
|
}
|
|
|
|
// Type-safe hooks
|
|
addFilter<ReactNode, [OrderFormData, SetState<OrderFormData>]>(
|
|
'woonoow_order_form_after_shipping',
|
|
(content, formData, setFormData) => {
|
|
// TypeScript knows the types!
|
|
return <SubdistrictSelector />;
|
|
}
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## **Available Hook Points**
|
|
|
|
### **Order Form Hooks**
|
|
```typescript
|
|
// Filter hooks (modify/add content)
|
|
'woonoow_order_form_data' // Modify form data before render
|
|
'woonoow_order_form_after_billing' // Add fields after billing address
|
|
'woonoow_order_form_after_shipping' // Add fields after shipping address
|
|
'woonoow_order_form_shipping_fields' // Add custom shipping fields
|
|
'woonoow_order_form_custom_sections' // Add custom sections
|
|
'woonoow_order_form_validation' // Add validation rules
|
|
|
|
// Action hooks (trigger events)
|
|
'woonoow_order_form_submit' // Before form submission
|
|
'woonoow_order_created' // After order created
|
|
'woonoow_order_updated' // After order updated
|
|
```
|
|
|
|
### **Settings Hooks**
|
|
```typescript
|
|
'woonoow_settings_tabs' // Add custom settings tabs
|
|
'woonoow_settings_sections' // Add settings sections
|
|
'woonoow_shipping_method_settings' // Modify shipping method settings
|
|
```
|
|
|
|
### **Product Hooks**
|
|
```typescript
|
|
'woonoow_product_form_fields' // Add custom product fields
|
|
'woonoow_product_meta_boxes' // Add meta boxes
|
|
```
|
|
|
|
---
|
|
|
|
## **Conclusion**
|
|
|
|
### **This is NOT "supporting specific 3rd party addons"**
|
|
|
|
✅ **This IS:**
|
|
- Providing a hook system
|
|
- Letting addons register themselves
|
|
- Rendering whatever addons provide
|
|
- Zero knowledge of specific addons
|
|
|
|
❌ **This is NOT:**
|
|
- Hardcoding addon components
|
|
- Importing addon modules
|
|
- Having addon-specific logic in core
|
|
|
|
### **Result:**
|
|
- WooNooW Core remains universal
|
|
- Addons can extend functionality
|
|
- No breaking changes if addon is removed
|
|
- Perfect separation of concerns! 🎯
|