docs: Hook system and Biteship addon specifications
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
This commit is contained in:
579
ADDON_HOOK_SYSTEM.md
Normal file
579
ADDON_HOOK_SYSTEM.md
Normal file
@@ -0,0 +1,579 @@
|
||||
# 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! 🎯
|
||||
Reference in New Issue
Block a user