Files
WooNooW/ADDON_HOOK_SYSTEM.md
dwindown 17afd3911f 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
2025-11-09 22:53:39 +07:00

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

  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)

// 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! 🎯