- Add horizontal padding to dialog content (px-1)
- Show real site URL from WNW_CONFIG.siteUrl instead of 'yoursite.com'
- Improve error handling to extract message from response.data.message
- Better slug generation regex (removes special chars properly)
- Reset form when modal closes
- Use theme colors (text-muted-foreground, hover:bg-accent/50)
- Implement serve_ssr_content with full PageSSR rendering
- SEO meta tags (title, description, og:*)
- Minimal CSS for bot-friendly presentation
- Yoast/Rank Math SEO data integration
- Add maybe_serve_ssr_for_bots hook (priority 2 on template_redirect)
- Serves SSR for structural pages with WooNooW structure
- Serves SSR for CPT items with templates
- Add use statements for PageSSR and PlaceholderRenderer
- Add Pages link to Appearance submenu in NavigationRegistry
- Bump NAV_VERSION to 1.1.0
- Add DynamicPageRenderer component for structural pages and CPT content
- Add 6 section components:
- HeroSection with multiple layout variants
- ContentSection for rich text/HTML content
- ImageTextSection with image-left/right layouts
- FeatureGridSection with grid-2/3/4 layouts
- CTABannerSection with color schemes
- ContactFormSection with webhook POST and redirect
- Add dynamic routes to App.tsx for /:slug and /:pathBase/:slug
- Build customer-spa successfully
- Add is_bot() detection in TemplateOverride.php (30+ bot patterns)
- Add PageSSR.php for server-side rendering of page sections
- Add PlaceholderRenderer.php for dynamic content resolution
- Add PagesController.php REST API for pages/templates CRUD
- Register PagesController routes in Routes.php
API Endpoints:
- GET /pages - list all pages/templates
- GET /pages/{slug} - get page structure
- POST /pages/{slug} - save page
- GET /templates/{cpt} - get CPT template
- POST /templates/{cpt} - save template
- GET /content/{type}/{slug} - get content with template applied
When SPA is frontpage, basePath is empty string. JavaScript || treats '' as falsy
and falls back to /store. Changed to ?? (nullish coalescing) so empty string works.
- Removed all debug logging (backend and frontend)
- Added filter hook 'woonoow_standard_checkout_field_keys' for extensibility
- Added form-row-wide class support to admin OrderForm
- Tax is automatically handled by WC's calculate_totals()
ROOT CAUSE: The sanitize_payload() method was returning a whitelist of
allowed fields, but shipping_cost, shipping_title, custom_fields, and
customer_note were NOT included. This caused these values to be null
even though the frontend was sending them correctly.
Added:
- shipping_cost (float)
- shipping_title (sanitized text)
- custom_fields (array)
- customer_note (sanitized textarea)
This should fix shipping not being applied to order totals.
- Added isFullWidthField helper to check for form-row-wide in class array
- Added getFieldWrapperClass helper to return md:col-span-2 for full width
- Applied dynamic width to billing_first_name and billing_last_name
- PHP can now control field width via class: ['form-row-wide']
- Added debug logging for shipping to help diagnose shipping not applied issue
Shipping Fix:
- Frontend now sends shipping_cost and shipping_title in order payload
- Backend uses these values as fallback when WC zone-based rate lookup fails
- Fixes issue where Rajaongkir and other API-based shipping wasn't applied
Dynamic Field Rendering:
- Added billingFields/shippingFields filters sorted by priority
- Added getBillingField/getShippingField helpers that return undefined for hidden fields
- All standard fields now conditionally rendered based on API response
- Fields use labels and required flags from API
- Any field can be hidden via PHP snippet (type: 'hidden' or hidden: true)
- Removed unused isFieldHidden function
1. ThankYou page - Added discount row to order summary
- All 3 template variations (receipt-inner, receipt-outer, default)
- Shows discount with negative amount in green when > 0
- Already had shipping/tax rows, now complete breakdown
2. Checkout - Enhanced isFieldHidden helper
- Now checks both type='hidden' AND hidden=true flags
- Any standard field can be hidden via PHP snippet
- Existing city/state/postcode/country checks unchanged
1. Hidden fields now properly hidden in SPA
- Added billing_postcode and shipping_postcode to isFieldHidden checks
- Fields with type='hidden' from PHP now conditionally rendered
2. Coupons now applied to order total
- Added coupons array to order submission payload
- CartController now calls calculate_totals() before reading discounts
- Returns per-coupon discount amounts {code, discount, type}
3. Shipping now applied to order total
- Already handled in submit() via find_shipping_rate_for_order
- Frontend now sends shipping_method in payload
4. Order details now include shipping/tracking info
- checkout/order/{id} API includes shipping_lines, tracking_number, tracking_url
- account/orders/{id} API includes same shipping/tracking fields
- Tracking info read from multiple plugin meta keys
5. Thank you/OrderDetails page shows shipping method and AWB
- Shipping Method section with courier name and cost
- AWB tracking for processing/completed orders with Track Shipment button
1. Hidden fields now respected in SPA
- Added isFieldHidden helper to check if PHP sets type to 'hidden'
- Country/state/city fields conditionally rendered based on API response
- Set default country value for hidden country fields (Indonesia-only stores)
2. Coupon discount now shows correct amount
- Added calculate_totals() before reading discount
- Changed coupons response to include {code, discount, type} per coupon
- Added discount_total at root level for frontend compatibility
3. Order details page now shows shipping info and AWB tracking
- Added shipping_lines, tracking_number, tracking_url to Order interface
- Added Shipping Method section with courier name and cost
- Added AWB tracking section for processing/completed orders
- Track Shipment button with link to tracking URL
RAJAONGKIR_INTEGRATION.md:
- Hide country/state/city when Indonesia is the only allowed country
- Make destination_id required for Indonesia-only stores
- Force country to ID in session bridge
- Added billing_destination_id fallback
SHIPPING_BRIDGE_PATTERN.md:
- New generic template for shipping provider integrations
- Documents architecture, hooks, and field types
- Provides copy-paste template for new providers
- Includes checklist for new integrations
Backend:
- Added /checkout/shipping-rates REST endpoint
- Returns available shipping methods from matching zone
- Triggers woonoow/shipping/before_calculate hook for Rajaongkir
Frontend:
- Added ShippingRate interface and state
- Added fetchShippingRates with 500ms debounce
- Replaced hardcoded shipping options with dynamic rates
- Added loading and empty state handling
- Added shipping_method to order submission payload
This fixes:
- Rajaongkir rates not appearing (now fetched from API)
- Free shipping showing despite disabled (now from WC zones)
- search_endpoint changed from '/woonoow/v1/rajaongkir/destinations'
to '/rajaongkir/destinations' (relative to API base)
- min_chars increased from 2 to 3 to reduce early API hits
Fixes:
1. SearchableSelect now supports onSearch prop for API-based search
- Added onSearch and isSearching props
- shouldFilter disabled when onSearch provided
2. DynamicCheckoutField connects handleApiSearch to SearchableSelect
3. RAJAONGKIR_INTEGRATION.md adds both billing and shipping destination_id
This enables the destination search field to actually call the API
when user types, instead of just filtering local (empty) options.
Root causes fixed:
1. API method: search_destination_api() not search_destination()
2. Field filter: removed country check that failed before user selection
3. Field now always added if store sells to Indonesia
The woocommerce_checkout_fields filter now:
- Checks if Rajaongkir is active
- Checks if Indonesia is in allowed countries
- Always adds field (frontend will show/hide based on country)
Based on deep analysis of Rajaongkir plugin:
- Destinations searched via RajaOngkir API (no local DB table)
- Uses Cekongkir_API::search_destination() for search
- Session 'selected_destination_id' required for rates
- Added SPA-aware checkout field injection
Code snippet includes:
1. REST endpoint /woonoow/v1/rajaongkir/destinations
2. Checkout field filter with REST context detection
3. Session bridge via woonoow/shipping/before_calculate
Issues fixed:
1. Country field was disabled when API failed (length 0)
- Changed: disabled={countries.length <= 1} → disabled={countries.length === 1}
- Only disables in single-country mode now
2. State field was disabled when no preloaded states
- Changed: Falls back to text input instead of disabled SearchableSelect
- Allows manual state entry for countries without state list
3. /countries API required admin permission
- Added public /countries endpoint to CheckoutController
- Uses permission_callback __return_true for customer checkout access
- Returns countries, states, and default_country
Backend (CheckoutController):
- Enhanced get_fields() API with custom_attributes, search_endpoint,
search_param, min_chars, input_class, default
- Supports new 'searchable_select' field type for API-backed search
Customer SPA:
- Created DynamicCheckoutField component for all field types
- Checkout fetches fields from /checkout/fields API
- Renders custom fields from PHP filters (billing + shipping)
- searchable_select type with live API search
- Custom field data included in checkout submission
This enables:
- Checkout Field Editor Pro compatibility
- Rajaongkir destination_id via simple code snippet
- Any plugin using woocommerce_checkout_fields filter
Updated RAJAONGKIR_INTEGRATION.md with code snippet approach.
Admin SPA:
- Changed billing/shipping state from Select to SearchableSelect
Customer SPA:
- Added cmdk package for command palette
- Created popover, command, and searchable-select UI components
- Added searchable country and state fields to checkout
- Fetches countries/states from /countries API
- Auto-clears state when country changes
Backend:
- Added generic woonoow/shipping/before_calculate hook
- Removed hardcoded Rajaongkir session handling
Documentation:
- Updated RAJAONGKIR_INTEGRATION.md with:
- Complete searchable destination selector plugin code
- JavaScript implementation
- React component version
- REST API endpoint for destination search
The wpAdminUrl config already includes admin.php?page=woonoow,
so constructing /admin.php?page=wc-settings on top of it was wrong.
Now uses siteUrl + /wp-admin for external WC links.
When WC packages array is empty, manually calculate rates by:
1. Get zone matching the shipping address
2. Call get_rates_for_package() on each method
3. Or fallback to method title/cost for simple methods like Free Shipping
Also added debug info to response to help diagnose issues.
Removed static method-level fallback. Shipping method selector now:
1. Shows 'Enter shipping address to see available rates' when address incomplete
2. Calls calculate_shipping endpoint to get actual WC_Shipping_Rate objects
3. Displays rate-level options (e.g., JNE REG, JNE YES) not method-level
This ensures third-party shipping plugins like Rajaongkir, UPS, FedEx
display their courier rates correctly.
1. Shipping method selector now shows static shippings list when
address is not complete, instead of 'No shipping methods available'.
Only shows the empty message when address IS complete but no methods
matched.
2. Variation selector in Dialog and Drawer now displays attribute names
(Size, Dispenser) in semibold and values (30ml, pump) in normal
weight for better visual hierarchy.
WooCommerce stores variation custom attribute meta keys in lowercase
(e.g., attribute_size), but we were using the original case from
parent attributes (e.g., attribute_Size). This caused empty attribute
values in the admin Order Form variation selector.
Fix: Use sanitize_title() to normalize the attribute name.
Instead of mounting to body (which breaks scoped styles), we now
mount the popover portal to #woonoow-admin-app. This ensures
dropdowns inherit the correct CSS variables and styling.
- Added global styles for [cmdk-root], [cmdk-list], [cmdk-item]
- Forced white background and border for [data-radix-popper-content-wrapper]
- Fixed missing styles that caused transparent/transparent dropdowns
- Guest users see wishlist icon in header (uses /wishlist page)
- Logged-in users don't see it (they use /my-account/wishlist instead)
- Applied to all 3 layout styles: Classic, Modern, Boutique
Root cause: LicensingModule::init() was called from within
plugins_loaded but then tried to add ANOTHER plugins_loaded action
for LicenseManager::init(). Since plugins_loaded already fired,
LicenseManager::init() never ran and WooCommerce order hooks
were never registered.
Fix: Call self::maybe_init_manager() directly instead of
scheduling via add_action.
- Added woocommerce_payment_complete hook
- Added woocommerce_thankyou hook for COD/virtual orders
- Added is_virtual_order helper to detect virtual-only orders
- generate_licenses_for_order now called from multiple hooks
(safe due to license_exists_for_order_item check)
1. Category Selection Bug:
- Added 'id' alias to category/tag API responses
- Frontend uses cat.id which was undefined (API returned term_id)
2. Checkout Redirect:
- Changed from window.location.href + reload to navigate()
- Added !isProcessing check to empty cart condition
3. Sidebar Shipping for Virtual-Only:
- Hide Shipping Method section when isVirtualOnly
- Hide Shipping row in totals when isVirtualOnly
4. License Table:
- Table creation runs via ensure_tables() on plugins_loaded
Added specific selectors:
- #woonoow-admin-app header
- #woonoow-admin-app nav
- #woonoow-admin-app [data-submenubar]
- #woonoow-admin-app [data-bottomnav]
These target the exact WooNooW app elements that need hiding.
Invoice Page (/orders/:id/invoice):
- A4-ready layout (210mm x 297mm)
- Store header, invoice number, QR code
- Billing/shipping address sections
- Styled items table with alternating rows
- Totals summary with conditional display
- Thank you footer
- Back to Order and Print buttons
Label Page (/orders/:id/label):
- 4x6 inch thermal label layout
- Ship To address with phone
- Items list (physical products only)
- Shipping method
- QR code for scanning
- Back to Order and Print buttons
Order Detail:
- Removed print-mode logic
- Removed print-only layouts
- Invoice/Label buttons now link to dedicated pages
- Label button still hidden for virtual-only orders
- Added license_duration_days field to ProductVariant type
- Added License Duration input to each variation card
- Backend: ProductsController saves/loads variation-level _license_duration_days meta
- Allows different license periods per variation (e.g., 1-year, 2-year, lifetime)