- ```
-
----
-
-## 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
- '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 (
-
-
Hello, WooNooW!
-
- );
-}
-```
-
-### 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
diff --git a/BITESHIP_ADDON_SPEC.md b/BITESHIP_ADDON_SPEC.md
new file mode 100644
index 0000000..057f3d2
--- /dev/null
+++ b/BITESHIP_ADDON_SPEC.md
@@ -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
+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
+ '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 (
+
+
+
+
+
+ );
+}
+```
+
+---
+
+## 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}
+
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
diff --git a/admin-spa/src/App.tsx b/admin-spa/src/App.tsx
index 87ffce7..dbc175e 100644
--- a/admin-spa/src/App.tsx
+++ b/admin-spa/src/App.tsx
@@ -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() {
} />
} />
} />
- } />
+ } />
} />
} />
} />
diff --git a/admin-spa/src/routes/Settings/Tax.tsx b/admin-spa/src/routes/Settings/Tax.tsx
new file mode 100644
index 0000000..5bc5581
--- /dev/null
+++ b/admin-spa/src/routes/Settings/Tax.tsx
@@ -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 (
+
+
+
+
+
+ );
+ }
+
+ return (
+ refetch()}
+ >
+
+ {__('Refresh')}
+
+ }
+ >
+
+ {/* Enable Tax Calculation */}
+
+ toggleMutation.mutate(checked)}
+ disabled={toggleMutation.isPending}
+ />
+
+
+ {/* Tax Rates */}
+ {settings?.calc_taxes === 'yes' && (
+
+
+
+
+
+
{__('Standard Rates')}
+
+ {__('Tax rates applied to standard products')}
+
+ {settings?.standard_rates && settings.standard_rates.length > 0 ? (
+
+ {settings.standard_rates.map((rate: any, index: number) => (
+
+ {rate.country} {rate.state && `- ${rate.state}`}
+ {rate.rate}%
+
+ ))}
+
+ ) : (
+
+ {__('No standard rates configured')}
+
+ )}
+
+
+
+
+
+
+
+
+
{__('Reduced Rates')}
+
+ {__('Lower tax rates for specific products')}
+
+ {settings?.reduced_rates && settings.reduced_rates.length > 0 ? (
+
+ {settings.reduced_rates.map((rate: any, index: number) => (
+
+ {rate.country} {rate.state && `- ${rate.state}`}
+ {rate.rate}%
+
+ ))}
+
+ ) : (
+
+ {__('No reduced rates configured')}
+
+ )}
+
+
+
+
+
+
+
+
+
{__('Zero Rates')}
+
+ {__('No tax for specific products or locations')}
+
+ {settings?.zero_rates && settings.zero_rates.length > 0 ? (
+
+ {settings.zero_rates.map((rate: any, index: number) => (
+
+ {rate.country} {rate.state && `- ${rate.state}`}
+ 0%
+
+ ))}
+
+ ) : (
+
+ {__('No zero rates configured')}
+
+ )}
+
+
+
+
+
+
+ )}
+
+ {/* Tax Options */}
+ {settings?.calc_taxes === 'yes' && (
+
+
+
+
+
{__('Prices entered with tax')}
+
+ {settings?.prices_include_tax === 'yes'
+ ? __('Product prices include tax')
+ : __('Product prices exclude tax')}
+
+
+
+
+
+
+
+
{__('Calculate tax based on')}
+
+ {settings?.tax_based_on === 'shipping' && __('Customer shipping address')}
+ {settings?.tax_based_on === 'billing' && __('Customer billing address')}
+ {settings?.tax_based_on === 'base' && __('Shop base address')}
+
+
+
+
+
+
+
+
{__('Display prices in shop')}
+
+ {settings?.tax_display_shop === 'incl' && __('Including tax')}
+ {settings?.tax_display_shop === 'excl' && __('Excluding tax')}
+
+
+
+
+
+
+ )}
+
+ {/* Advanced Settings Link */}
+
+
+
+ );
+}
diff --git a/includes/Api/Routes.php b/includes/Api/Routes.php
index 737fa40..defa7c7 100644
--- a/includes/Api/Routes.php
+++ b/includes/Api/Routes.php
@@ -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();
});
}
}
diff --git a/includes/Api/TaxController.php b/includes/Api/TaxController.php
new file mode 100644
index 0000000..36029a0
--- /dev/null
+++ b/includes/Api/TaxController.php
@@ -0,0 +1,156 @@
+ 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;
+ }
+}