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
This commit is contained in:
715
ADDON_DEVELOPMENT_GUIDE.md
Normal file
715
ADDON_DEVELOPMENT_GUIDE.md
Normal file
@@ -0,0 +1,715 @@
|
||||
# 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.**
|
||||
@@ -1,579 +0,0 @@
|
||||
# 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! 🎯
|
||||
@@ -1,726 +0,0 @@
|
||||
# WooNooW Addon Injection Guide
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Last Updated:** 2025-10-28
|
||||
**Status:** Production Ready
|
||||
|
||||
---
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Admin SPA Addons](#admin-spa-addons)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Addon Registration](#addon-registration)
|
||||
- [Route Registration](#route-registration)
|
||||
- [Navigation Injection](#navigation-injection)
|
||||
- [Component Development](#component-development)
|
||||
- [Best Practices](#best-practices)
|
||||
3. [Customer SPA Addons](#customer-spa-addons) *(Coming Soon)*
|
||||
4. [Testing & Debugging](#testing--debugging)
|
||||
5. [Examples](#examples)
|
||||
6. [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
WooNooW provides a **powerful addon injection system** that allows third-party plugins to seamlessly integrate with the React-powered admin SPA. Addons can:
|
||||
|
||||
- ✅ Register custom SPA routes
|
||||
- ✅ Inject navigation menu items
|
||||
- ✅ Add submenu items to existing sections
|
||||
- ✅ Load React components dynamically
|
||||
- ✅ Declare dependencies and capabilities
|
||||
- ✅ Maintain full isolation and safety
|
||||
|
||||
**No iframes, no hacks, just clean React integration!**
|
||||
|
||||
---
|
||||
|
||||
## Admin SPA Addons
|
||||
|
||||
### Quick Start
|
||||
|
||||
**5-Minute Integration:**
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: My WooNooW Addon
|
||||
* Description: Adds custom functionality to WooNooW
|
||||
* Version: 1.0.0
|
||||
*/
|
||||
|
||||
// 1. Register your addon
|
||||
add_filter('woonoow/addon_registry', function($addons) {
|
||||
$addons['my-addon'] = [
|
||||
'id' => 'my-addon',
|
||||
'name' => 'My Addon',
|
||||
'version' => '1.0.0',
|
||||
'author' => 'Your Name',
|
||||
'description' => 'My awesome addon',
|
||||
'spa_bundle' => plugin_dir_url(__FILE__) . 'dist/addon.js',
|
||||
'dependencies' => ['woocommerce' => '8.0'],
|
||||
];
|
||||
return $addons;
|
||||
});
|
||||
|
||||
// 2. Register your routes
|
||||
add_filter('woonoow/spa_routes', function($routes) {
|
||||
$routes[] = [
|
||||
'path' => '/my-addon',
|
||||
'component_url' => plugin_dir_url(__FILE__) . 'dist/MyAddonPage.js',
|
||||
'capability' => 'manage_woocommerce',
|
||||
'title' => 'My Addon',
|
||||
];
|
||||
return $routes;
|
||||
});
|
||||
|
||||
// 3. Add navigation item
|
||||
add_filter('woonoow/nav_tree', function($tree) {
|
||||
$tree[] = [
|
||||
'key' => 'my-addon',
|
||||
'label' => 'My Addon',
|
||||
'path' => '/my-addon',
|
||||
'icon' => 'puzzle', // lucide icon name
|
||||
'children' => [],
|
||||
];
|
||||
return $tree;
|
||||
});
|
||||
```
|
||||
|
||||
**That's it!** Your addon is now integrated into WooNooW.
|
||||
|
||||
---
|
||||
|
||||
### Addon Registration
|
||||
|
||||
**Filter:** `woonoow/addon_registry`
|
||||
**Priority:** 20 (runs on `plugins_loaded`)
|
||||
**File:** `includes/Compat/AddonRegistry.php`
|
||||
|
||||
#### Configuration Schema
|
||||
|
||||
```php
|
||||
add_filter('woonoow/addon_registry', function($addons) {
|
||||
$addons['addon-id'] = [
|
||||
// Required
|
||||
'id' => 'addon-id', // Unique identifier
|
||||
'name' => 'Addon Name', // Display name
|
||||
'version' => '1.0.0', // Semantic version
|
||||
|
||||
// Optional
|
||||
'author' => 'Author Name', // Author name
|
||||
'description' => 'Description', // Short description
|
||||
'spa_bundle' => 'https://...', // Main JS bundle URL
|
||||
|
||||
// Dependencies (optional)
|
||||
'dependencies' => [
|
||||
'woocommerce' => '8.0', // Min WooCommerce version
|
||||
'wordpress' => '6.0', // Min WordPress version
|
||||
],
|
||||
|
||||
// Advanced (optional)
|
||||
'routes' => [], // Route definitions
|
||||
'nav_items' => [], // Nav item definitions
|
||||
'widgets' => [], // Widget definitions
|
||||
];
|
||||
return $addons;
|
||||
});
|
||||
```
|
||||
|
||||
#### Dependency Validation
|
||||
|
||||
WooNooW automatically validates dependencies:
|
||||
|
||||
```php
|
||||
'dependencies' => [
|
||||
'woocommerce' => '8.0', // Requires WooCommerce 8.0+
|
||||
'wordpress' => '6.4', // Requires WordPress 6.4+
|
||||
]
|
||||
```
|
||||
|
||||
If dependencies are not met:
|
||||
- ❌ Addon is disabled automatically
|
||||
- ❌ Routes are not registered
|
||||
- ❌ Navigation items are hidden
|
||||
|
||||
---
|
||||
|
||||
### Route Registration
|
||||
|
||||
**Filter:** `woonoow/spa_routes`
|
||||
**Priority:** 25 (runs on `plugins_loaded`)
|
||||
**File:** `includes/Compat/RouteRegistry.php`
|
||||
|
||||
#### Basic Route
|
||||
|
||||
```php
|
||||
add_filter('woonoow/spa_routes', function($routes) {
|
||||
$routes[] = [
|
||||
'path' => '/subscriptions',
|
||||
'component_url' => plugin_dir_url(__FILE__) . 'dist/SubscriptionsList.js',
|
||||
'capability' => 'manage_woocommerce',
|
||||
'title' => 'Subscriptions',
|
||||
];
|
||||
return $routes;
|
||||
});
|
||||
```
|
||||
|
||||
#### Multiple 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' => 'All Subscriptions',
|
||||
];
|
||||
|
||||
$routes[] = [
|
||||
'path' => '/subscriptions/new',
|
||||
'component_url' => $base_url . 'SubscriptionNew.js',
|
||||
'capability' => 'manage_woocommerce',
|
||||
'title' => 'New Subscription',
|
||||
];
|
||||
|
||||
$routes[] = [
|
||||
'path' => '/subscriptions/:id',
|
||||
'component_url' => $base_url . 'SubscriptionDetail.js',
|
||||
'capability' => 'manage_woocommerce',
|
||||
'title' => 'Subscription Detail',
|
||||
];
|
||||
|
||||
return $routes;
|
||||
});
|
||||
```
|
||||
|
||||
#### Route Configuration
|
||||
|
||||
| Property | Type | Required | Description |
|
||||
|----------|------|----------|-------------|
|
||||
| `path` | string | ✅ Yes | Route path (must start with `/`) |
|
||||
| `component_url` | string | ✅ Yes | URL to React component JS file |
|
||||
| `capability` | string | No | WordPress capability (default: `manage_woocommerce`) |
|
||||
| `title` | string | No | Page title |
|
||||
| `exact` | boolean | No | Exact path match (default: `false`) |
|
||||
| `props` | object | No | Props to pass to component |
|
||||
|
||||
---
|
||||
|
||||
### Navigation Injection
|
||||
|
||||
#### Add Main Menu Item
|
||||
|
||||
**Filter:** `woonoow/nav_tree`
|
||||
**Priority:** 30 (runs on `plugins_loaded`)
|
||||
**File:** `includes/Compat/NavigationRegistry.php`
|
||||
|
||||
```php
|
||||
add_filter('woonoow/nav_tree', function($tree) {
|
||||
$tree[] = [
|
||||
'key' => 'subscriptions',
|
||||
'label' => __('Subscriptions', 'my-addon'),
|
||||
'path' => '/subscriptions',
|
||||
'icon' => 'repeat', // lucide-react icon name
|
||||
'children' => [
|
||||
[
|
||||
'label' => __('All Subscriptions', 'my-addon'),
|
||||
'mode' => 'spa',
|
||||
'path' => '/subscriptions',
|
||||
],
|
||||
[
|
||||
'label' => __('New', 'my-addon'),
|
||||
'mode' => 'spa',
|
||||
'path' => '/subscriptions/new',
|
||||
],
|
||||
],
|
||||
];
|
||||
return $tree;
|
||||
});
|
||||
```
|
||||
|
||||
#### Inject into Existing Section
|
||||
|
||||
**Filter:** `woonoow/nav_tree/{key}/children`
|
||||
|
||||
```php
|
||||
// Add "Bundles" to Products menu
|
||||
add_filter('woonoow/nav_tree/products/children', function($children) {
|
||||
$children[] = [
|
||||
'label' => __('Bundles', 'my-addon'),
|
||||
'mode' => 'spa',
|
||||
'path' => '/products/bundles',
|
||||
];
|
||||
return $children;
|
||||
});
|
||||
|
||||
// Add "Reports" to Dashboard menu
|
||||
add_filter('woonoow/nav_tree/dashboard/children', function($children) {
|
||||
$children[] = [
|
||||
'label' => __('Custom Reports', 'my-addon'),
|
||||
'mode' => 'spa',
|
||||
'path' => '/reports',
|
||||
];
|
||||
return $children;
|
||||
});
|
||||
```
|
||||
|
||||
#### Available Sections
|
||||
|
||||
| Key | Label | Path |
|
||||
|-----|-------|------|
|
||||
| `dashboard` | Dashboard | `/` |
|
||||
| `orders` | Orders | `/orders` |
|
||||
| `products` | Products | `/products` |
|
||||
| `coupons` | Coupons | `/coupons` |
|
||||
| `customers` | Customers | `/customers` |
|
||||
| `settings` | Settings | `/settings` |
|
||||
|
||||
#### Navigation Item Schema
|
||||
|
||||
```typescript
|
||||
{
|
||||
key: string; // Unique key (for main items)
|
||||
label: string; // Display label (i18n recommended)
|
||||
path: string; // Route path
|
||||
icon?: string; // Lucide icon name (main items only)
|
||||
mode: 'spa' | 'bridge'; // Render mode
|
||||
href?: string; // External URL (bridge mode)
|
||||
exact?: boolean; // Exact path match
|
||||
children?: SubItem[]; // Submenu items
|
||||
}
|
||||
```
|
||||
|
||||
#### Lucide Icons
|
||||
|
||||
WooNooW uses [lucide-react](https://lucide.dev/) icons (16-20px, 1.5px stroke).
|
||||
|
||||
**Popular icons:**
|
||||
- `layout-dashboard` - Dashboard
|
||||
- `receipt-text` - Orders
|
||||
- `package` - Products
|
||||
- `tag` - Coupons
|
||||
- `users` - Customers
|
||||
- `settings` - Settings
|
||||
- `repeat` - Subscriptions
|
||||
- `calendar` - Bookings
|
||||
- `credit-card` - Payments
|
||||
- `bar-chart` - Analytics
|
||||
|
||||
---
|
||||
|
||||
### Component Development
|
||||
|
||||
#### Component Structure
|
||||
|
||||
Your React component will be dynamically imported and rendered:
|
||||
|
||||
```typescript
|
||||
// dist/MyAddonPage.tsx
|
||||
import React from 'react';
|
||||
|
||||
export default function MyAddonPage(props: any) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-lg border border-border p-6 bg-card">
|
||||
<h2 className="text-xl font-semibold mb-2">My Addon</h2>
|
||||
<p className="text-sm opacity-70">Welcome to my addon!</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);
|
||||
console.log('Symbol:', store.currency_symbol);
|
||||
|
||||
// Access site info
|
||||
const wnw = (window as any).wnw;
|
||||
console.log('Site Title:', wnw.siteTitle);
|
||||
console.log('Admin URL:', wnw.adminUrl);
|
||||
```
|
||||
|
||||
#### 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 MyAddonPage() {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h2>{__('My Addon', 'my-addon')}</h2>
|
||||
<p>{formatMoney(1234.56)}</p>
|
||||
<Button>{__('Click Me', 'my-addon')}</Button>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Build Your Component
|
||||
|
||||
**Using Vite:**
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
lib: {
|
||||
entry: 'src/MyAddonPage.tsx',
|
||||
name: 'MyAddon',
|
||||
fileName: 'MyAddonPage',
|
||||
formats: ['es'],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['react', 'react-dom'],
|
||||
output: {
|
||||
globals: {
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Best Practices
|
||||
|
||||
#### ✅ DO:
|
||||
|
||||
1. **Use Semantic Versioning**
|
||||
```php
|
||||
'version' => '1.2.3'
|
||||
```
|
||||
|
||||
2. **Declare Dependencies**
|
||||
```php
|
||||
'dependencies' => ['woocommerce' => '8.0']
|
||||
```
|
||||
|
||||
3. **Check Capabilities**
|
||||
```php
|
||||
'capability' => 'manage_woocommerce'
|
||||
```
|
||||
|
||||
4. **Internationalize Strings**
|
||||
```php
|
||||
'label' => __('Subscriptions', 'my-addon')
|
||||
```
|
||||
|
||||
5. **Use Namespaced Hooks**
|
||||
```php
|
||||
add_filter('woonoow/addon_registry', ...)
|
||||
```
|
||||
|
||||
6. **Validate User Input**
|
||||
```php
|
||||
$value = sanitize_text_field($_POST['value']);
|
||||
```
|
||||
|
||||
7. **Handle Errors Gracefully**
|
||||
```typescript
|
||||
try {
|
||||
// Load component
|
||||
} catch (error) {
|
||||
// Show error message
|
||||
}
|
||||
```
|
||||
|
||||
8. **Follow WooNooW UI Patterns**
|
||||
- Use Tailwind CSS classes
|
||||
- Use Shadcn UI components
|
||||
- Follow mobile-first design
|
||||
- Use `.ui-ctrl` class for controls
|
||||
|
||||
#### ❌ DON'T:
|
||||
|
||||
1. **Don't Hardcode URLs**
|
||||
```php
|
||||
// ❌ Bad
|
||||
'component_url' => 'https://mysite.com/addon.js'
|
||||
|
||||
// ✅ Good
|
||||
'component_url' => plugin_dir_url(__FILE__) . 'dist/addon.js'
|
||||
```
|
||||
|
||||
2. **Don't Skip Capability Checks**
|
||||
```php
|
||||
// ❌ Bad
|
||||
'capability' => ''
|
||||
|
||||
// ✅ Good
|
||||
'capability' => 'manage_woocommerce'
|
||||
```
|
||||
|
||||
3. **Don't Use Generic Hook Names**
|
||||
```php
|
||||
// ❌ Bad
|
||||
add_filter('addon_registry', ...)
|
||||
|
||||
// ✅ Good
|
||||
add_filter('woonoow/addon_registry', ...)
|
||||
```
|
||||
|
||||
4. **Don't Modify Core Navigation**
|
||||
```php
|
||||
// ❌ Bad - Don't remove core items
|
||||
unset($tree[0]);
|
||||
|
||||
// ✅ Good - Add your own items
|
||||
$tree[] = ['key' => 'my-addon', ...];
|
||||
```
|
||||
|
||||
5. **Don't Block the Main Thread**
|
||||
```typescript
|
||||
// ❌ Bad
|
||||
while (loading) { /* wait */ }
|
||||
|
||||
// ✅ Good
|
||||
if (loading) return <Loader />;
|
||||
```
|
||||
|
||||
6. **Don't Use Inline Styles**
|
||||
```typescript
|
||||
// ❌ Bad
|
||||
<div style={{color: 'red'}}>
|
||||
|
||||
// ✅ Good
|
||||
<div className="text-red-600">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Customer SPA Addons
|
||||
|
||||
**Status:** 🚧 Coming Soon
|
||||
|
||||
Customer SPA addon injection will support:
|
||||
- Cart page customization
|
||||
- Checkout step injection
|
||||
- My Account page tabs
|
||||
- Widget areas
|
||||
- Custom forms
|
||||
|
||||
**Stay tuned for updates!**
|
||||
|
||||
---
|
||||
|
||||
## Testing & Debugging
|
||||
|
||||
### Enable Debug Mode
|
||||
|
||||
```php
|
||||
// wp-config.php
|
||||
define('WNW_DEV', true);
|
||||
```
|
||||
|
||||
This enables:
|
||||
- ✅ Console logging
|
||||
- ✅ Cache flushing
|
||||
- ✅ Detailed error messages
|
||||
|
||||
### Check Addon Registration
|
||||
|
||||
```javascript
|
||||
// Browser console
|
||||
console.log(window.WNW_ADDONS);
|
||||
console.log(window.WNW_ADDON_ROUTES);
|
||||
console.log(window.WNW_NAV_TREE);
|
||||
```
|
||||
|
||||
### Flush Caches
|
||||
|
||||
```php
|
||||
// Programmatically
|
||||
do_action('woonoow_flush_caches');
|
||||
|
||||
// Or via URL (admins only)
|
||||
// https://yoursite.com/wp-admin/?flush_wnw_cache=1
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Addon not appearing?**
|
||||
- Check dependencies are met
|
||||
- Verify capability requirements
|
||||
- Check browser console for errors
|
||||
- Flush caches
|
||||
|
||||
**Route not loading?**
|
||||
- Verify `component_url` is correct
|
||||
- Check file exists and is accessible
|
||||
- Look for JS errors in console
|
||||
- Ensure component exports default
|
||||
|
||||
**Navigation not showing?**
|
||||
- Check filter priority
|
||||
- Verify path matches route
|
||||
- Check i18n strings load
|
||||
- Inspect `window.WNW_NAV_TREE`
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Simple Addon
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: WooNooW Hello World
|
||||
* Description: Minimal addon example
|
||||
* Version: 1.0.0
|
||||
*/
|
||||
|
||||
add_filter('woonoow/addon_registry', function($addons) {
|
||||
$addons['hello-world'] = [
|
||||
'id' => 'hello-world',
|
||||
'name' => 'Hello World',
|
||||
'version' => '1.0.0',
|
||||
];
|
||||
return $addons;
|
||||
});
|
||||
|
||||
add_filter('woonoow/spa_routes', function($routes) {
|
||||
$routes[] = [
|
||||
'path' => '/hello',
|
||||
'component_url' => plugin_dir_url(__FILE__) . 'dist/Hello.js',
|
||||
'capability' => 'read', // All logged-in users
|
||||
'title' => 'Hello World',
|
||||
];
|
||||
return $routes;
|
||||
});
|
||||
|
||||
add_filter('woonoow/nav_tree', function($tree) {
|
||||
$tree[] = [
|
||||
'key' => 'hello',
|
||||
'label' => 'Hello',
|
||||
'path' => '/hello',
|
||||
'icon' => 'smile',
|
||||
'children' => [],
|
||||
];
|
||||
return $tree;
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
// dist/Hello.tsx
|
||||
import React from 'react';
|
||||
|
||||
export default function Hello() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold">Hello, WooNooW!</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Full-Featured Addon
|
||||
|
||||
See `ADDON_INJECTION_READINESS_REPORT.md` for the complete Subscriptions addon example.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Addon Registry Issues
|
||||
|
||||
**Problem:** Addon not registered
|
||||
|
||||
**Solutions:**
|
||||
1. Check `plugins_loaded` hook fires
|
||||
2. Verify filter name: `woonoow/addon_registry`
|
||||
3. Check dependencies are met
|
||||
4. Look for PHP errors in debug log
|
||||
|
||||
### Route Issues
|
||||
|
||||
**Problem:** Route returns 404
|
||||
|
||||
**Solutions:**
|
||||
1. Verify path starts with `/`
|
||||
2. Check `component_url` is accessible
|
||||
3. Ensure route is registered before navigation
|
||||
4. Check capability requirements
|
||||
|
||||
### Navigation Issues
|
||||
|
||||
**Problem:** Menu item not showing
|
||||
|
||||
**Solutions:**
|
||||
1. Check filter: `woonoow/nav_tree` or `woonoow/nav_tree/{key}/children`
|
||||
2. Verify path matches registered route
|
||||
3. Check i18n strings are loaded
|
||||
4. Inspect `window.WNW_NAV_TREE` in console
|
||||
|
||||
### Component Loading Issues
|
||||
|
||||
**Problem:** Component fails to load
|
||||
|
||||
**Solutions:**
|
||||
1. Check component exports `default`
|
||||
2. Verify file is built correctly
|
||||
3. Check for JS errors in console
|
||||
4. Ensure React/ReactDOM are available
|
||||
5. Test component URL directly in browser
|
||||
|
||||
---
|
||||
|
||||
## Support & Resources
|
||||
|
||||
**Documentation:**
|
||||
- `ADDON_INJECTION_READINESS_REPORT.md` - Technical analysis
|
||||
- `ADDONS_ADMIN_UI_REQUIREMENTS.md` - Requirements & status
|
||||
- `PROGRESS_NOTE.md` - Development progress
|
||||
|
||||
**Code References:**
|
||||
- `includes/Compat/AddonRegistry.php` - Addon registration
|
||||
- `includes/Compat/RouteRegistry.php` - Route management
|
||||
- `includes/Compat/NavigationRegistry.php` - Navigation building
|
||||
- `admin-spa/src/App.tsx` - Dynamic route loading
|
||||
- `admin-spa/src/nav/tree.ts` - Navigation tree
|
||||
|
||||
**Community:**
|
||||
- GitHub Issues: Report bugs
|
||||
- Discussions: Ask questions
|
||||
- Examples: Share your addons
|
||||
|
||||
---
|
||||
|
||||
**End of Guide**
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Last Updated:** 2025-10-28
|
||||
**Status:** ✅ Production Ready
|
||||
260
BITESHIP_ADDON_SPEC.md
Normal file
260
BITESHIP_ADDON_SPEC.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# WooNooW Indonesia Shipping (Biteship Integration)
|
||||
|
||||
## Plugin Specification
|
||||
|
||||
**Plugin Name:** WooNooW Indonesia Shipping
|
||||
**Description:** Simple Indonesian shipping integration using Biteship Rate API
|
||||
**Version:** 1.0.0
|
||||
**Requires:** WooNooW 1.0.0+, WooCommerce 8.0+
|
||||
**License:** GPL v2 or later
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
A lightweight shipping plugin that integrates Biteship's Rate API with WooNooW SPA, providing:
|
||||
- ✅ Indonesian address fields (Province, City, District, Subdistrict)
|
||||
- ✅ Real-time shipping rate calculation
|
||||
- ✅ Multiple courier support (JNE, SiCepat, J&T, AnterAja, etc.)
|
||||
- ✅ Works in both frontend checkout AND admin order form
|
||||
- ✅ No subscription required (uses free Biteship Rate API)
|
||||
|
||||
---
|
||||
|
||||
## Features Roadmap
|
||||
|
||||
### Phase 1: Core Functionality
|
||||
- [ ] WooCommerce Shipping Method integration
|
||||
- [ ] Biteship Rate API integration
|
||||
- [ ] Indonesian address database (Province → Subdistrict)
|
||||
- [ ] Frontend checkout integration
|
||||
- [ ] Admin settings page
|
||||
|
||||
### Phase 2: SPA Integration
|
||||
- [ ] REST API endpoints for address data
|
||||
- [ ] REST API for rate calculation
|
||||
- [ ] React components (SubdistrictSelector, CourierSelector)
|
||||
- [ ] Hook integration with WooNooW OrderForm
|
||||
- [ ] Admin order form support
|
||||
|
||||
### Phase 3: Advanced Features
|
||||
- [ ] Rate caching (reduce API calls)
|
||||
- [ ] Custom rate markup
|
||||
- [ ] Free shipping threshold
|
||||
- [ ] Multi-origin support
|
||||
- [ ] Shipping label generation (optional, requires paid Biteship plan)
|
||||
|
||||
---
|
||||
|
||||
## Plugin Structure
|
||||
|
||||
```
|
||||
woonoow-indonesia-shipping/
|
||||
├── woonoow-indonesia-shipping.php # Main plugin file
|
||||
├── includes/
|
||||
│ ├── class-shipping-method.php # WooCommerce shipping method
|
||||
│ ├── class-biteship-api.php # Biteship API client
|
||||
│ ├── class-address-database.php # Indonesian address data
|
||||
│ ├── class-addon-integration.php # WooNooW addon integration
|
||||
│ └── Api/
|
||||
│ └── AddressController.php # REST API endpoints
|
||||
├── admin/
|
||||
│ ├── class-settings.php # Admin settings page
|
||||
│ └── views/
|
||||
│ └── settings-page.php # Settings UI
|
||||
├── admin-spa/
|
||||
│ ├── src/
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── SubdistrictSelector.tsx # Address selector
|
||||
│ │ │ └── CourierSelector.tsx # Courier selection
|
||||
│ │ ├── hooks/
|
||||
│ │ │ ├── useAddressData.ts # Fetch address data
|
||||
│ │ │ └── useRateCalculation.ts # Calculate rates
|
||||
│ │ └── index.ts # Addon registration
|
||||
│ ├── package.json
|
||||
│ └── vite.config.ts
|
||||
├── data/
|
||||
│ └── indonesia-areas.sql # Address database dump
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
```sql
|
||||
CREATE TABLE `wp_woonoow_indonesia_areas` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`biteship_area_id` varchar(50) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`type` enum('province','city','district','subdistrict') NOT NULL,
|
||||
`parent_id` bigint(20) DEFAULT NULL,
|
||||
`postal_code` varchar(10) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `biteship_area_id` (`biteship_area_id`),
|
||||
KEY `parent_id` (`parent_id`),
|
||||
KEY `type` (`type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WooCommerce Shipping Method
|
||||
|
||||
```php
|
||||
<?php
|
||||
// includes/class-shipping-method.php
|
||||
|
||||
class WooNooW_Indonesia_Shipping_Method extends WC_Shipping_Method {
|
||||
|
||||
public function __construct($instance_id = 0) {
|
||||
$this->id = 'woonoow_indonesia_shipping';
|
||||
$this->instance_id = absint($instance_id);
|
||||
$this->method_title = __('Indonesia Shipping', 'woonoow-indonesia-shipping');
|
||||
$this->supports = array('shipping-zones', 'instance-settings');
|
||||
$this->init();
|
||||
}
|
||||
|
||||
public function init_form_fields() {
|
||||
$this->instance_form_fields = array(
|
||||
'api_key' => array(
|
||||
'title' => 'Biteship API Key',
|
||||
'type' => 'text'
|
||||
),
|
||||
'origin_subdistrict_id' => array(
|
||||
'title' => 'Origin Subdistrict',
|
||||
'type' => 'select',
|
||||
'options' => $this->get_subdistrict_options()
|
||||
),
|
||||
'couriers' => array(
|
||||
'title' => 'Available Couriers',
|
||||
'type' => 'multiselect',
|
||||
'options' => array(
|
||||
'jne' => 'JNE',
|
||||
'sicepat' => 'SiCepat',
|
||||
'jnt' => 'J&T Express'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function calculate_shipping($package = array()) {
|
||||
$origin = $this->get_option('origin_subdistrict_id');
|
||||
$destination = $package['destination']['subdistrict_id'] ?? null;
|
||||
|
||||
if (!$origin || !$destination) return;
|
||||
|
||||
$api = new WooNooW_Biteship_API($this->get_option('api_key'));
|
||||
$rates = $api->get_rates($origin, $destination, $package);
|
||||
|
||||
foreach ($rates as $rate) {
|
||||
$this->add_rate(array(
|
||||
'id' => $this->id . ':' . $rate['courier_code'],
|
||||
'label' => $rate['courier_name'] . ' - ' . $rate['service_name'],
|
||||
'cost' => $rate['price']
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
```php
|
||||
<?php
|
||||
// includes/Api/AddressController.php
|
||||
|
||||
register_rest_route('woonoow/v1', '/indonesia-shipping/provinces', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => 'get_provinces'
|
||||
));
|
||||
|
||||
register_rest_route('woonoow/v1', '/indonesia-shipping/calculate-rates', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => 'calculate_rates'
|
||||
));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## React Components
|
||||
|
||||
```typescript
|
||||
// admin-spa/src/components/SubdistrictSelector.tsx
|
||||
|
||||
export function SubdistrictSelector({ value, onChange }) {
|
||||
const [provinceId, setProvinceId] = useState('');
|
||||
const [cityId, setCityId] = useState('');
|
||||
|
||||
const { data: provinces } = useQuery({
|
||||
queryKey: ['provinces'],
|
||||
queryFn: () => api.get('/indonesia-shipping/provinces')
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<Select label="Province" options={provinces} />
|
||||
<Select label="City" options={cities} />
|
||||
<Select label="Subdistrict" onChange={onChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WooNooW Hook Integration
|
||||
|
||||
```typescript
|
||||
// admin-spa/src/index.ts
|
||||
|
||||
import { addonLoader, addFilter } from '@woonoow/hooks';
|
||||
|
||||
addonLoader.register({
|
||||
id: 'indonesia-shipping',
|
||||
name: 'Indonesia Shipping',
|
||||
version: '1.0.0',
|
||||
init: () => {
|
||||
// Add subdistrict selector in order form
|
||||
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 }
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Timeline
|
||||
|
||||
**Week 1: Backend**
|
||||
- Day 1-2: Database schema + address data import
|
||||
- Day 3-4: WooCommerce shipping method class
|
||||
- Day 5: Biteship API integration
|
||||
|
||||
**Week 2: Frontend**
|
||||
- Day 1-2: REST API endpoints
|
||||
- Day 3-4: React components
|
||||
- Day 5: Hook integration + testing
|
||||
|
||||
**Week 3: Polish**
|
||||
- Day 1-2: Error handling + loading states
|
||||
- Day 3: Rate caching
|
||||
- Day 4-5: Documentation + testing
|
||||
|
||||
---
|
||||
|
||||
**Status:** Specification Complete - Ready for Implementation
|
||||
@@ -196,6 +196,7 @@ import SettingsIndex from '@/routes/Settings';
|
||||
import SettingsStore from '@/routes/Settings/Store';
|
||||
import SettingsPayments from '@/routes/Settings/Payments';
|
||||
import SettingsShipping from '@/routes/Settings/Shipping';
|
||||
import SettingsTax from '@/routes/Settings/Tax';
|
||||
import MorePage from '@/routes/More';
|
||||
|
||||
// Addon Route Component - Dynamically loads addon components
|
||||
@@ -426,7 +427,7 @@ function AppRoutes() {
|
||||
<Route path="/settings/store" element={<SettingsStore />} />
|
||||
<Route path="/settings/payments" element={<SettingsPayments />} />
|
||||
<Route path="/settings/shipping" element={<SettingsShipping />} />
|
||||
<Route path="/settings/taxes" element={<SettingsIndex />} />
|
||||
<Route path="/settings/tax" element={<SettingsTax />} />
|
||||
<Route path="/settings/checkout" element={<SettingsIndex />} />
|
||||
<Route path="/settings/customers" element={<SettingsIndex />} />
|
||||
<Route path="/settings/notifications" element={<SettingsIndex />} />
|
||||
|
||||
309
admin-spa/src/routes/Settings/Tax.tsx
Normal file
309
admin-spa/src/routes/Settings/Tax.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import React from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '@/lib/api';
|
||||
import { SettingsLayout } from './components/SettingsLayout';
|
||||
import { SettingsCard } from './components/SettingsCard';
|
||||
import { ToggleField } from './components/ToggleField';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ExternalLink, RefreshCw } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { __ } from '@/lib/i18n';
|
||||
|
||||
export default function TaxSettings() {
|
||||
const queryClient = useQueryClient();
|
||||
const wcAdminUrl = (window as any).WNW_CONFIG?.wpAdminUrl || '/wp-admin';
|
||||
|
||||
// Fetch tax settings
|
||||
const { data: settings, isLoading, refetch } = useQuery({
|
||||
queryKey: ['tax-settings'],
|
||||
queryFn: () => api.get('/settings/tax'),
|
||||
});
|
||||
|
||||
// Toggle tax calculation
|
||||
const toggleMutation = useMutation({
|
||||
mutationFn: async (enabled: boolean) => {
|
||||
return api.post('/settings/tax/toggle', { enabled });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tax-settings'] });
|
||||
toast.success(__('Tax settings updated'));
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error?.message || __('Failed to update tax settings'));
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SettingsLayout
|
||||
title={__('Tax')}
|
||||
description={__('Configure tax calculation and rates')}
|
||||
>
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsLayout
|
||||
title={__('Tax')}
|
||||
description={__('Configure tax calculation and rates')}
|
||||
action={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => refetch()}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
{__('Refresh')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* Enable Tax Calculation */}
|
||||
<SettingsCard
|
||||
title={__('Tax Calculation')}
|
||||
description={__('Enable or disable tax calculation for your store')}
|
||||
>
|
||||
<ToggleField
|
||||
label={__('Enable tax rates and calculations')}
|
||||
description={__('When enabled, taxes will be calculated based on customer location and product tax class')}
|
||||
checked={settings?.calc_taxes === 'yes'}
|
||||
onChange={(checked) => toggleMutation.mutate(checked)}
|
||||
disabled={toggleMutation.isPending}
|
||||
/>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Tax Rates */}
|
||||
{settings?.calc_taxes === 'yes' && (
|
||||
<SettingsCard
|
||||
title={__('Tax Rates')}
|
||||
description={__('Configure tax rates for different locations and tax classes')}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border p-4 bg-muted/50">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium mb-1">{__('Standard Rates')}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{__('Tax rates applied to standard products')}
|
||||
</p>
|
||||
{settings?.standard_rates && settings.standard_rates.length > 0 ? (
|
||||
<div className="mt-3 space-y-2">
|
||||
{settings.standard_rates.map((rate: any, index: number) => (
|
||||
<div key={index} className="flex items-center justify-between text-sm">
|
||||
<span>{rate.country} {rate.state && `- ${rate.state}`}</span>
|
||||
<span className="font-medium">{rate.rate}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{__('No standard rates configured')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=tax§ion=standard`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
{__('Manage')}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border p-4 bg-muted/50">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium mb-1">{__('Reduced Rates')}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{__('Lower tax rates for specific products')}
|
||||
</p>
|
||||
{settings?.reduced_rates && settings.reduced_rates.length > 0 ? (
|
||||
<div className="mt-3 space-y-2">
|
||||
{settings.reduced_rates.map((rate: any, index: number) => (
|
||||
<div key={index} className="flex items-center justify-between text-sm">
|
||||
<span>{rate.country} {rate.state && `- ${rate.state}`}</span>
|
||||
<span className="font-medium">{rate.rate}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{__('No reduced rates configured')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=tax§ion=reduced-rate`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
{__('Manage')}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border p-4 bg-muted/50">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium mb-1">{__('Zero Rates')}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{__('No tax for specific products or locations')}
|
||||
</p>
|
||||
{settings?.zero_rates && settings.zero_rates.length > 0 ? (
|
||||
<div className="mt-3 space-y-2">
|
||||
{settings.zero_rates.map((rate: any, index: number) => (
|
||||
<div key={index} className="flex items-center justify-between text-sm">
|
||||
<span>{rate.country} {rate.state && `- ${rate.state}`}</span>
|
||||
<span className="font-medium">0%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{__('No zero rates configured')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=tax§ion=zero-rate`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
{__('Manage')}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
)}
|
||||
|
||||
{/* Tax Options */}
|
||||
{settings?.calc_taxes === 'yes' && (
|
||||
<SettingsCard
|
||||
title={__('Tax Options')}
|
||||
description={__('Additional tax calculation settings')}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<div>
|
||||
<p className="font-medium text-sm">{__('Prices entered with tax')}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{settings?.prices_include_tax === 'yes'
|
||||
? __('Product prices include tax')
|
||||
: __('Product prices exclude tax')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=tax`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{__('Change')}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2 border-t">
|
||||
<div>
|
||||
<p className="font-medium text-sm">{__('Calculate tax based on')}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{settings?.tax_based_on === 'shipping' && __('Customer shipping address')}
|
||||
{settings?.tax_based_on === 'billing' && __('Customer billing address')}
|
||||
{settings?.tax_based_on === 'base' && __('Shop base address')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=tax`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{__('Change')}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2 border-t">
|
||||
<div>
|
||||
<p className="font-medium text-sm">{__('Display prices in shop')}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{settings?.tax_display_shop === 'incl' && __('Including tax')}
|
||||
{settings?.tax_display_shop === 'excl' && __('Excluding tax')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=tax`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{__('Change')}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
)}
|
||||
|
||||
{/* Advanced Settings Link */}
|
||||
<div className="rounded-lg border border-dashed p-6 text-center">
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{__('For advanced tax configuration, use the WooCommerce settings page')}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=tax`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 mr-2" />
|
||||
{__('Open Tax Settings in WooCommerce')}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use WooNooW\Api\AuthController;
|
||||
use WooNooW\API\PaymentsController;
|
||||
use WooNooW\API\StoreController;
|
||||
use WooNooW\Api\ShippingController;
|
||||
use WooNooW\Api\TaxController;
|
||||
|
||||
class Routes {
|
||||
public static function init() {
|
||||
@@ -54,6 +55,10 @@ class Routes {
|
||||
// Shipping controller
|
||||
$shipping_controller = new ShippingController();
|
||||
$shipping_controller->register_routes();
|
||||
|
||||
// Tax controller
|
||||
$tax_controller = new TaxController();
|
||||
$tax_controller->register_routes();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
156
includes/Api/TaxController.php
Normal file
156
includes/Api/TaxController.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
/**
|
||||
* Tax Settings REST API Controller
|
||||
*
|
||||
* @package WooNooW
|
||||
*/
|
||||
|
||||
namespace WooNooW\Api;
|
||||
|
||||
use WP_REST_Controller;
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_Error;
|
||||
|
||||
class TaxController extends WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public function register_routes() {
|
||||
$namespace = 'woonoow/v1';
|
||||
|
||||
// Get tax settings
|
||||
register_rest_route(
|
||||
$namespace,
|
||||
'/settings/tax',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_settings' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
)
|
||||
);
|
||||
|
||||
// Toggle tax calculation
|
||||
register_rest_route(
|
||||
$namespace,
|
||||
'/settings/tax/toggle',
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'toggle_tax' ),
|
||||
'permission_callback' => array( $this, 'check_permission' ),
|
||||
'args' => array(
|
||||
'enabled' => array(
|
||||
'required' => true,
|
||||
'type' => 'boolean',
|
||||
'sanitize_callback' => 'rest_sanitize_boolean',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check permission
|
||||
*/
|
||||
public function check_permission() {
|
||||
return current_user_can( 'manage_woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tax settings
|
||||
*/
|
||||
public function get_settings( WP_REST_Request $request ) {
|
||||
try {
|
||||
$settings = array(
|
||||
'calc_taxes' => get_option( 'woocommerce_calc_taxes', 'no' ),
|
||||
'prices_include_tax' => get_option( 'woocommerce_prices_include_tax', 'no' ),
|
||||
'tax_based_on' => get_option( 'woocommerce_tax_based_on', 'shipping' ),
|
||||
'tax_display_shop' => get_option( 'woocommerce_tax_display_shop', 'excl' ),
|
||||
'tax_display_cart' => get_option( 'woocommerce_tax_display_cart', 'excl' ),
|
||||
'standard_rates' => $this->get_tax_rates( 'standard' ),
|
||||
'reduced_rates' => $this->get_tax_rates( 'reduced-rate' ),
|
||||
'zero_rates' => $this->get_tax_rates( 'zero-rate' ),
|
||||
);
|
||||
|
||||
return new WP_REST_Response( $settings, 200 );
|
||||
} catch ( \Exception $e ) {
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
'error' => 'fetch_failed',
|
||||
'message' => $e->getMessage(),
|
||||
),
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle tax calculation
|
||||
*/
|
||||
public function toggle_tax( WP_REST_Request $request ) {
|
||||
try {
|
||||
$enabled = $request->get_param( 'enabled' );
|
||||
$value = $enabled ? 'yes' : 'no';
|
||||
|
||||
update_option( 'woocommerce_calc_taxes', $value );
|
||||
|
||||
// Clear WooCommerce cache
|
||||
\WC_Cache_Helper::invalidate_cache_group( 'taxes' );
|
||||
\WC_Cache_Helper::get_transient_version( 'shipping', true );
|
||||
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
'success' => true,
|
||||
'enabled' => $enabled,
|
||||
'message' => $enabled
|
||||
? __( 'Tax calculation enabled', 'woonoow' )
|
||||
: __( 'Tax calculation disabled', 'woonoow' ),
|
||||
),
|
||||
200
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
'error' => 'update_failed',
|
||||
'message' => $e->getMessage(),
|
||||
),
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tax rates for a specific class
|
||||
*/
|
||||
private function get_tax_rates( $tax_class ) {
|
||||
global $wpdb;
|
||||
|
||||
$rates = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates
|
||||
WHERE tax_rate_class = %s
|
||||
ORDER BY tax_rate_order ASC",
|
||||
$tax_class
|
||||
)
|
||||
);
|
||||
|
||||
$formatted_rates = array();
|
||||
|
||||
foreach ( $rates as $rate ) {
|
||||
$formatted_rates[] = array(
|
||||
'id' => $rate->tax_rate_id,
|
||||
'country' => $rate->tax_rate_country,
|
||||
'state' => $rate->tax_rate_state,
|
||||
'rate' => $rate->tax_rate,
|
||||
'name' => $rate->tax_rate_name,
|
||||
'priority' => $rate->tax_rate_priority,
|
||||
'compound' => $rate->tax_rate_compound,
|
||||
'shipping' => $rate->tax_rate_shipping,
|
||||
);
|
||||
}
|
||||
|
||||
return $formatted_rates;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user