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
15 KiB
15 KiB
WooNooW Addon Hook System
Problem Statement
Question: How can WooNooW SPA support addons without hardcoding specific components?
Example of WRONG approach:
// ❌ 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
// 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
// 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
// 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
// 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'));
// 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
/**
* 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
// 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
- WordPress loads
woonoow-indonesia-shipping.php - Addon registers with
AddonRegistry - WooNooW enqueues addon script (
addon.js) - Addon script runs
addonLoader.register() - Addon hooks are registered
- OrderForm renders → hooks fire → addon components appear
Result: ✅ Subdistrict selector appears in order form
Scenario 2: Addon is NOT Installed
- No addon plugin loaded
- No addon script enqueued
- No hooks registered
- OrderForm renders → hooks fire → return
null
Result: ✅ No error, form works normally without addon fields
Key Differences from Hardcoding
❌ Hardcoding (WRONG)
// 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)
// 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)
// 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
// 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
'woonoow_settings_tabs' // Add custom settings tabs
'woonoow_settings_sections' // Add settings sections
'woonoow_shipping_method_settings' // Modify shipping method settings
Product Hooks
'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! 🎯