## 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
716 lines
16 KiB
Markdown
716 lines
16 KiB
Markdown
# WooNooW Addon Development Guide
|
|
|
|
**Version:** 2.0.0
|
|
**Last Updated:** November 9, 2025
|
|
**Status:** Production Ready
|
|
|
|
---
|
|
|
|
## 📋 Table of Contents
|
|
|
|
1. [Overview](#overview)
|
|
2. [Addon Types](#addon-types)
|
|
3. [Quick Start](#quick-start)
|
|
4. [SPA Route Injection](#spa-route-injection)
|
|
5. [Hook System Integration](#hook-system-integration)
|
|
6. [Component Development](#component-development)
|
|
7. [Best Practices](#best-practices)
|
|
8. [Examples](#examples)
|
|
9. [Troubleshooting](#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
|
|
|
|
```php
|
|
// 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
|
|
|
|
```typescript
|
|
// Registers hooks
|
|
addFilter('woonoow_order_form_after_shipping', ...);
|
|
addAction('woonoow_order_created', ...);
|
|
```
|
|
|
|
### Type C: Full-Featured Addon (Both Systems)
|
|
**Use when:** Complex integration needed
|
|
|
|
**Example:** Subscriptions, Bookings, Memberships
|
|
|
|
```php
|
|
// 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
|
|
<?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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```bash
|
|
npm run build
|
|
```
|
|
|
|
**Done!** Your addon is now integrated.
|
|
|
|
---
|
|
|
|
## SPA Route Injection
|
|
|
|
### Register Routes
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
// 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
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
// Before form submission
|
|
'woonoow_order_form_submit'
|
|
|
|
// After order created
|
|
'woonoow_order_created'
|
|
|
|
// After order updated
|
|
'woonoow_order_updated'
|
|
```
|
|
|
|
### Hook Registration Example
|
|
|
|
```typescript
|
|
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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
// 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**
|
|
```typescript
|
|
addFilter<ReactNode, [OrderFormData, SetState<OrderFormData>]>(
|
|
'woonoow_order_form_after_shipping',
|
|
(content, formData, setFormData) => {
|
|
// TypeScript knows the types!
|
|
return <MyComponent />;
|
|
}
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## Component Development
|
|
|
|
### Basic Component
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```javascript
|
|
// 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**
|
|
```typescript
|
|
// ✅ Good - No hardcoding
|
|
addFilter('woonoow_order_form_after_shipping', ...);
|
|
```
|
|
|
|
2. **Use Route Injection for New Pages**
|
|
```php
|
|
// ✅ Good - Separate UI
|
|
add_filter('woonoow/spa_routes', ...);
|
|
```
|
|
|
|
3. **Declare Dependencies**
|
|
```php
|
|
'dependencies' => ['woocommerce' => '8.0']
|
|
```
|
|
|
|
4. **Check Capabilities**
|
|
```php
|
|
'capability' => 'manage_woocommerce'
|
|
```
|
|
|
|
5. **Internationalize Strings**
|
|
```php
|
|
'label' => __('My Addon', 'my-addon')
|
|
```
|
|
|
|
6. **Handle Errors Gracefully**
|
|
```typescript
|
|
try {
|
|
await api.post(...);
|
|
} catch (error) {
|
|
toast.error('Failed to save');
|
|
}
|
|
```
|
|
|
|
### ❌ DON'T:
|
|
|
|
1. **Don't Hardcode Addon Components in Core**
|
|
```typescript
|
|
// ❌ 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**
|
|
```php
|
|
// ❌ Bad
|
|
'capability' => ''
|
|
|
|
// ✅ Good
|
|
'capability' => 'manage_woocommerce'
|
|
```
|
|
|
|
3. **Don't Modify Core Navigation**
|
|
```php
|
|
// ❌ Bad
|
|
unset($tree[0]);
|
|
|
|
// ✅ Good
|
|
$tree[] = ['key' => 'my-addon', ...];
|
|
```
|
|
|
|
---
|
|
|
|
## Examples
|
|
|
|
### Example 1: Simple UI Addon (Route Injection Only)
|
|
|
|
```php
|
|
<?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)
|
|
|
|
```typescript
|
|
// 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>
|
|
</>
|
|
);
|
|
});
|
|
}
|
|
});
|
|
```
|
|
|
|
### Example 3: Full-Featured Addon (Both Systems)
|
|
|
|
```php
|
|
<?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;
|
|
});
|
|
```
|
|
|
|
```typescript
|
|
// 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.**
|