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:
dwindown
2025-11-09 22:53:39 +07:00
parent d1b2c6e562
commit 17afd3911f

579
ADDON_HOOK_SYSTEM.md Normal file
View 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! 🎯