Files
WooNooW/ADDON_DEVELOPMENT_GUIDE.md
dwindown 603d94b73c feat: Tax settings + unified addon guide + Biteship spec
## 1. Created BITESHIP_ADDON_SPEC.md 
- Complete plugin specification
- Database schema, API endpoints
- WooCommerce integration
- React components
- Implementation timeline

## 2. Merged Addon Documentation 
Created ADDON_DEVELOPMENT_GUIDE.md (single source of truth):
- Merged ADDON_INJECTION_GUIDE.md + ADDON_HOOK_SYSTEM.md
- Two addon types: Route Injection + Hook System
- Clear examples for each type
- Best practices and troubleshooting
- Deleted old documents

## 3. Tax Settings 
Frontend (admin-spa/src/routes/Settings/Tax.tsx):
- Enable/disable tax calculation toggle
- Display standard/reduced/zero tax rates
- Show tax options (prices include tax, based on, display)
- Link to WooCommerce for advanced config
- Clean, simple UI

Backend (includes/Api/TaxController.php):
- GET /settings/tax - Fetch tax settings
- POST /settings/tax/toggle - Enable/disable taxes
- Fetches rates from woocommerce_tax_rates table
- Clears WooCommerce cache on update

## 4. Advanced Local Pickup - TODO
Will be simple: Admin adds multiple pickup locations

## Key Decisions:
 Hook system = No hardcoding, zero coupling
 Tax settings = Simple toggle + view, advanced in WC
 Single addon guide = One source of truth

Next: Advanced Local Pickup locations
2025-11-09 23:13:52 +07:00

16 KiB

WooNooW Addon Development Guide

Version: 2.0.0
Last Updated: November 9, 2025
Status: Production Ready


📋 Table of Contents

  1. Overview
  2. Addon Types
  3. Quick Start
  4. SPA Route Injection
  5. Hook System Integration
  6. Component Development
  7. Best Practices
  8. Examples
  9. Troubleshooting

Overview

WooNooW provides two powerful addon systems:

1. SPA Route Injection (Admin UI)

  • Register custom SPA routes
  • Inject navigation menu items
  • Add submenu items to existing sections
  • Load React components dynamically
  • Full isolation and safety

2. Hook System (Functional Extension)

  • Extend OrderForm, ProductForm, etc.
  • Add custom fields and validation
  • Inject components at specific points
  • Zero coupling with core
  • WordPress-style filters and actions

Both systems work together seamlessly!


Addon Types

Type A: UI-Only Addon (Route Injection)

Use when: Adding new pages/sections to admin

Example: Reports, Analytics, Custom Dashboard

// Registers routes + navigation
add_filter('woonoow/spa_routes', ...);
add_filter('woonoow/nav_tree', ...);

Type B: Functional Addon (Hook System)

Use when: Extending existing functionality

Example: Indonesia Shipping, Custom Fields, Validation

// Registers hooks
addFilter('woonoow_order_form_after_shipping', ...);
addAction('woonoow_order_created', ...);

Use when: Complex integration needed

Example: Subscriptions, Bookings, Memberships

// Backend: Routes + Hooks
add_filter('woonoow/spa_routes', ...);
add_filter('woonoow/nav_tree', ...);

// Frontend: Hook registration
addonLoader.register({
  init: () => {
    addFilter('woonoow_order_form_custom_sections', ...);
  }
});

Quick Start

Step 1: Create Plugin File

<?php
/**
 * Plugin Name: My WooNooW Addon
 * Description: Extends WooNooW functionality
 * Version: 1.0.0
 * Requires: WooNooW 1.0.0+
 */

// 1. Register addon
add_filter('woonoow/addon_registry', function($addons) {
    $addons['my-addon'] = [
        'id'           => 'my-addon',
        'name'         => 'My Addon',
        'version'      => '1.0.0',
        'spa_bundle'   => plugin_dir_url(__FILE__) . 'dist/addon.js',
        'dependencies' => ['woocommerce' => '8.0'],
    ];
    return $addons;
});

// 2. Register routes (optional - for UI pages)
add_filter('woonoow/spa_routes', function($routes) {
    $routes[] = [
        'path'          => '/my-addon',
        'component_url' => plugin_dir_url(__FILE__) . 'dist/MyPage.js',
        'capability'    => 'manage_woocommerce',
        'title'         => 'My Addon',
    ];
    return $routes;
});

// 3. Add navigation (optional - for UI pages)
add_filter('woonoow/nav_tree', function($tree) {
    $tree[] = [
        'key'   => 'my-addon',
        'label' => 'My Addon',
        'path'  => '/my-addon',
        'icon'  => 'puzzle',
    ];
    return $tree;
});

Step 2: Create Frontend Integration

// admin-spa/src/index.ts

import { addonLoader, addFilter } from '@woonoow/hooks';

addonLoader.register({
  id: 'my-addon',
  name: 'My Addon',
  version: '1.0.0',
  init: () => {
    // Register hooks here
    addFilter('woonoow_order_form_custom_sections', (content, formData, setFormData) => {
      return (
        <>
          {content}
          <MyCustomSection data={formData} onChange={setFormData} />
        </>
      );
    });
  }
});

Step 3: Build

npm run build

Done! Your addon is now integrated.


SPA Route Injection

Register Routes

add_filter('woonoow/spa_routes', function($routes) {
    $base_url = plugin_dir_url(__FILE__) . 'dist/';
    
    $routes[] = [
        'path'          => '/subscriptions',
        'component_url' => $base_url . 'SubscriptionsList.js',
        'capability'    => 'manage_woocommerce',
        'title'         => 'Subscriptions',
    ];
    
    $routes[] = [
        'path'          => '/subscriptions/:id',
        'component_url' => $base_url . 'SubscriptionDetail.js',
        'capability'    => 'manage_woocommerce',
        'title'         => 'Subscription Detail',
    ];
    
    return $routes;
});

Add Navigation

// Main menu item
add_filter('woonoow/nav_tree', function($tree) {
    $tree[] = [
        'key'      => 'subscriptions',
        'label'    => __('Subscriptions', 'my-addon'),
        'path'     => '/subscriptions',
        'icon'     => 'repeat',
        'children' => [
            [
                'label' => __('All Subscriptions', 'my-addon'),
                'mode'  => 'spa',
                'path'  => '/subscriptions',
            ],
            [
                'label' => __('New', 'my-addon'),
                'mode'  => 'spa',
                'path'  => '/subscriptions/new',
            ],
        ],
    ];
    return $tree;
});

// Or inject into existing section
add_filter('woonoow/nav_tree/products/children', function($children) {
    $children[] = [
        'label' => __('Bundles', 'my-addon'),
        'mode'  => 'spa',
        'path'  => '/products/bundles',
    ];
    return $children;
});

Hook System Integration

Available Hooks

Order Form Hooks

// Add fields after billing address
'woonoow_order_form_after_billing'

// Add fields after shipping address
'woonoow_order_form_after_shipping'

// Add custom shipping fields
'woonoow_order_form_shipping_fields'

// Add custom sections
'woonoow_order_form_custom_sections'

// Add validation rules
'woonoow_order_form_validation'

// Modify form data before render
'woonoow_order_form_data'

Action Hooks

// Before form submission
'woonoow_order_form_submit'

// After order created
'woonoow_order_created'

// After order updated
'woonoow_order_updated'

Hook Registration Example

import { addonLoader, addFilter, addAction } from '@woonoow/hooks';

addonLoader.register({
  id: 'indonesia-shipping',
  name: 'Indonesia Shipping',
  version: '1.0.0',
  init: () => {
    // Filter: Add subdistrict selector
    addFilter('woonoow_order_form_after_shipping', (content, formData, setFormData) => {
      return (
        <>
          {content}
          <SubdistrictSelector
            value={formData.shipping?.subdistrict_id}
            onChange={(id) => setFormData({
              ...formData,
              shipping: { ...formData.shipping, subdistrict_id: id }
            })}
          />
        </>
      );
    });

    // Filter: Add validation
    addFilter('woonoow_order_form_validation', (errors, formData) => {
      if (!formData.shipping?.subdistrict_id) {
        errors.subdistrict = 'Subdistrict is required';
      }
      return errors;
    });

    // Action: Log when order created
    addAction('woonoow_order_created', (orderId, orderData) => {
      console.log('Order created:', orderId);
    });
  }
});

Hook System Benefits

Zero Coupling

// WooNooW Core has no knowledge of your addon
{applyFilters('woonoow_order_form_after_shipping', null, formData, setFormData)}

// If addon exists: Returns your component
// If addon doesn't exist: Returns null
// No import, no error!

Multiple Addons Can Hook

// Addon A
addFilter('woonoow_order_form_after_shipping', (content) => {
  return <>{content}<AddonAFields /></>;
});

// Addon B
addFilter('woonoow_order_form_after_shipping', (content) => {
  return <>{content}<AddonBFields /></>;
});

// Both render!

Type Safety

addFilter<ReactNode, [OrderFormData, SetState<OrderFormData>]>(
  'woonoow_order_form_after_shipping',
  (content, formData, setFormData) => {
    // TypeScript knows the types!
    return <MyComponent />;
  }
);

Component Development

Basic Component

// dist/MyPage.tsx
import React from 'react';

export default function MyPage() {
  return (
    <div className="space-y-6">
      <div className="rounded-lg border p-6 bg-card">
        <h2 className="text-xl font-semibold mb-2">My Addon</h2>
        <p className="text-sm opacity-70">Welcome!</p>
      </div>
    </div>
  );
}

Access WooNooW APIs

// Access REST API
const api = (window as any).WNW_API;
const response = await fetch(`${api.root}my-addon/endpoint`, {
  headers: { 'X-WP-Nonce': api.nonce },
});

// Access store data
const store = (window as any).WNW_STORE;
console.log('Currency:', store.currency);

// Access site info
const wnw = (window as any).wnw;
console.log('Site Title:', wnw.siteTitle);

Use WooNooW Components

import { __ } from '@/lib/i18n';
import { formatMoney } from '@/lib/currency';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';

export default function MyPage() {
  return (
    <Card className="p-6">
      <h2>{__('My Addon', 'my-addon')}</h2>
      <p>{formatMoney(1234.56)}</p>
      <Button>{__('Click Me', 'my-addon')}</Button>
    </Card>
  );
}

Build Configuration

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    lib: {
      entry: 'src/index.ts',
      name: 'MyAddon',
      fileName: 'addon',
      formats: ['es'],
    },
    rollupOptions: {
      external: ['react', 'react-dom'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
    },
  },
});

Best Practices

DO:

  1. Use Hook System for Functional Extensions

    // ✅ Good - No hardcoding
    addFilter('woonoow_order_form_after_shipping', ...);
    
  2. Use Route Injection for New Pages

    // ✅ Good - Separate UI
    add_filter('woonoow/spa_routes', ...);
    
  3. Declare Dependencies

    'dependencies' => ['woocommerce' => '8.0']
    
  4. Check Capabilities

    'capability' => 'manage_woocommerce'
    
  5. Internationalize Strings

    'label' => __('My Addon', 'my-addon')
    
  6. Handle Errors Gracefully

    try {
      await api.post(...);
    } catch (error) {
      toast.error('Failed to save');
    }
    

DON'T:

  1. Don't Hardcode Addon Components in Core

    // ❌ Bad - Breaks if addon not installed
    import { SubdistrictSelector } from 'addon';
    <SubdistrictSelector />
    
    // ✅ Good - Use hooks
    {applyFilters('woonoow_order_form_after_shipping', null)}
    
  2. Don't Skip Capability Checks

    // ❌ Bad
    'capability' => ''
    
    // ✅ Good
    'capability' => 'manage_woocommerce'
    
  3. Don't Modify Core Navigation

    // ❌ Bad
    unset($tree[0]);
    
    // ✅ Good
    $tree[] = ['key' => 'my-addon', ...];
    

Examples

Example 1: Simple UI Addon (Route Injection Only)

<?php
/**
 * Plugin Name: WooNooW Reports
 * Description: Custom reports page
 */

add_filter('woonoow/addon_registry', function($addons) {
    $addons['reports'] = [
        'id'      => 'reports',
        'name'    => 'Reports',
        'version' => '1.0.0',
    ];
    return $addons;
});

add_filter('woonoow/spa_routes', function($routes) {
    $routes[] = [
        'path'          => '/reports',
        'component_url' => plugin_dir_url(__FILE__) . 'dist/Reports.js',
        'title'         => 'Reports',
    ];
    return $routes;
});

add_filter('woonoow/nav_tree', function($tree) {
    $tree[] = [
        'key'   => 'reports',
        'label' => 'Reports',
        'path'  => '/reports',
        'icon'  => 'bar-chart',
    ];
    return $tree;
});

Example 2: Functional Addon (Hook System Only)

// Indonesia Shipping - No UI pages, just extends OrderForm

import { addonLoader, addFilter } from '@woonoow/hooks';
import { SubdistrictSelector } from './components/SubdistrictSelector';

addonLoader.register({
  id: 'indonesia-shipping',
  name: 'Indonesia Shipping',
  version: '1.0.0',
  init: () => {
    addFilter('woonoow_order_form_after_shipping', (content, formData, setFormData) => {
      return (
        <>
          {content}
          <div className="border rounded-lg p-4 mt-4">
            <h3 className="font-medium mb-3">📍 Shipping Destination</h3>
            <SubdistrictSelector
              value={formData.shipping?.subdistrict_id}
              onChange={(id) => setFormData({
                ...formData,
                shipping: { ...formData.shipping, subdistrict_id: id }
              })}
            />
          </div>
        </>
      );
    });
  }
});
<?php
/**
 * Plugin Name: WooNooW Subscriptions
 * Description: Subscription management
 */

// Backend: Register addon + routes
add_filter('woonoow/addon_registry', function($addons) {
    $addons['subscriptions'] = [
        'id'         => 'subscriptions',
        'name'       => 'Subscriptions',
        'version'    => '1.0.0',
        'spa_bundle' => plugin_dir_url(__FILE__) . 'dist/addon.js',
    ];
    return $addons;
});

add_filter('woonoow/spa_routes', function($routes) {
    $routes[] = [
        'path'          => '/subscriptions',
        'component_url' => plugin_dir_url(__FILE__) . 'dist/SubscriptionsList.js',
    ];
    return $routes;
});

add_filter('woonoow/nav_tree', function($tree) {
    $tree[] = [
        'key'   => 'subscriptions',
        'label' => 'Subscriptions',
        'path'  => '/subscriptions',
        'icon'  => 'repeat',
    ];
    return $tree;
});
// Frontend: Hook integration

import { addonLoader, addFilter } from '@woonoow/hooks';

addonLoader.register({
  id: 'subscriptions',
  name: 'Subscriptions',
  version: '1.0.0',
  init: () => {
    // Add subscription fields to order form
    addFilter('woonoow_order_form_custom_sections', (content, formData, setFormData) => {
      return (
        <>
          {content}
          <SubscriptionOptions data={formData} onChange={setFormData} />
        </>
      );
    });

    // Add subscription fields to product form
    addFilter('woonoow_product_form_fields', (content, formData, setFormData) => {
      return (
        <>
          {content}
          <SubscriptionSettings data={formData} onChange={setFormData} />
        </>
      );
    });
  }
});

Troubleshooting

Addon Not Appearing?

  • Check dependencies are met
  • Verify capability requirements
  • Check browser console for errors
  • Flush caches: ?flush_wnw_cache=1

Route Not Loading?

  • Verify component_url is correct
  • Check file exists and is accessible
  • Look for JS errors in console
  • Ensure component exports default

Hook Not Firing?

  • Check hook name is correct
  • Verify addon is registered
  • Check window.WNW_ADDONS in console
  • Ensure init() function runs

Component Not Rendering?

  • Check for React errors in console
  • Verify component returns valid JSX
  • Check props are passed correctly
  • Test component in isolation

Support & Resources

Documentation:

  • ADDON_INJECTION_GUIDE.md - SPA route injection (legacy)
  • ADDON_HOOK_SYSTEM.md - Hook system details (legacy)
  • BITESHIP_ADDON_SPEC.md - Indonesia shipping example
  • SHIPPING_ADDON_RESEARCH.md - Shipping integration patterns

Code References:

  • includes/Compat/AddonRegistry.php - Addon registration
  • includes/Compat/RouteRegistry.php - Route management
  • includes/Compat/NavigationRegistry.php - Navigation building
  • admin-spa/src/lib/hooks.ts - Hook system implementation
  • admin-spa/src/App.tsx - Dynamic route loading

End of Guide

Version: 2.0.0
Last Updated: November 9, 2025
Status: Production Ready

This is the single source of truth for WooNooW addon development.