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)
Copy Cart/Checkout Links:
- Added to GeneralTab for simple products (same pattern as variations)
- Link generation with add-to-cart and redirect params
Licensing Settings:
- 'Enable licensing for this product' checkbox in Additional Options
- License settings panel: activation limit, duration (days)
- State management in ProductFormTabbed
- Backend: ProductsController saves/loads licensing meta fields
Backend:
- _licensing_enabled, _license_activation_limit, _license_duration_days post meta
1. Admin Store Link - Add to WP admin bar (Menu.php) with proper option check
2. Activity Log - Fix Loading text to show correct state after data loads
3. Avatar Upload - Use correct option key woonoow_allow_custom_avatar
4. Downloadable Files - Connect to WooCommerce native:
- Add downloads array to format_product_full
- Add downloads/download_limit/download_expiry handling in update_product
- Add downloads handling in create_product
Customer Avatar Upload:
- Add /account/avatar endpoint for upload/delete
- Add /account/avatar-settings endpoint for settings
- Update AccountDetails.tsx with avatar upload UI
- Support base64 image upload with validation
Product Downloadable Files:
- Create DownloadsTab component for file management
- Add downloads state to ProductFormTabbed
- Show Downloads tab when 'downloadable' is checked
- Support file name, URL, download limit, and expiry
- Add Store link to admin header (visible when customer SPA is enabled)
- Add storeUrl and customerSpaEnabled to WNW_CONFIG in Assets.php and StandaloneAdmin.php
- Update window.d.ts with new WNW_CONFIG properties
- Create ActivityLog.tsx component with search, filters, and pagination
- Add /notifications/logs API endpoint to NotificationsController
- Update Notifications.tsx to link to activity log page
- Add ActivityLog route to App.tsx
1. Add allow_custom_avatar toggle to Customer Settings
2. Implement coupon apply/remove in Cart and Checkout pages
3. Update Cart interface with coupons array and discount_total
4. Implement Downloads page to fetch from /account/downloads API
- Created Newsletter/index.tsx as tabs container
- Extracted Newsletter/Subscribers.tsx (from old Newsletter.tsx)
- Moved Campaigns to Newsletter/Campaigns.tsx
- Updated App.tsx routes (campaigns now under newsletter)
- Removed separate Campaigns card from Marketing index
- Follows Customer Notifications tab pattern for consistency
- Sticky not possible when page is inside overflow-auto container
- Using standard flexbox layout where sidebar and content scroll together
- Separate mobile (fixed overlay) and desktop (inline) sidebars
- Clean, simple layout matching typical documentation patterns
- Fixed sidebar to not use inset-y-0 (was overriding top offset)
- Mobile: fixed positioning with sidebarTopClass
- Desktop: lg:sticky for proper sticky behavior
- Added X-WP-Nonce header to docs API fetch calls in Help page
- Fixed build-production.sh to include docs/ folder (changed --exclude='*.md' to --exclude='/*.md')
- This allows root-level docs like README.md to be excluded while keeping docs/ folder
Phase 1: Core Documentation
- Created docs/ folder with 8 markdown documentation files
- Getting Started, Installation, Troubleshooting, FAQ
- Configuration docs (Appearance, SPA Mode)
- Feature docs (Shop, Checkout)
- PHP registry with filter hook for addon extensibility
Phase 2: Documentation Viewer
- DocsController.php with REST API endpoints
- GET /woonoow/v1/docs - List all docs (with addon hook)
- GET /woonoow/v1/docs/{slug} - Get document content
- Admin SPA /help route with sidebar navigation
- Markdown rendering with react-markdown
- Added Help & Docs to More page for mobile access
Filter Hook: woonoow_docs_registry
Addons can register their own documentation sections.
- Created ResetPassword.tsx with:
- Password reset form with strength indicator
- Key validation on load
- Show/hide password toggle
- Success/error states
- Redirect to login on success
- Updated EmailManager.php:
- Changed reset_link from wp-login.php to SPA route
- Format: /wp-admin/admin.php?page=woonoow#/reset-password?key=KEY&login=LOGIN
- Added AuthController API methods:
- validate_reset_key: Validates reset key before showing form
- reset_password: Performs actual password reset
- Registered new REST routes in Routes.php:
- POST /auth/validate-reset-key
- POST /auth/reset-password
Password reset emails now link to the SPA instead of native WordPress.
ROOT CAUSE (from screenshot DevTools):
href="<span style=...>[login_url]</span>" - HTML span inside href attribute!
Flow causing the bug:
1. parseCardsForPreview converts [button url="{login_url}"] to <a href="{login_url}">
2. sampleData replacement runs but login_url NOT in sampleData
3. Variable highlighting injects <span>[login_url]</span> INTO href="..."
4. HTML is completely broken
FIXES APPLIED:
1. Added missing URL variables to sampleData:
- login_url, reset_link, reset_key
- user_login, user_email, user_temp_password
- customer_first_name, customer_last_name
2. Changed variable highlighting from HTML spans to plain text [variable]
- Prevents breaking HTML attributes if variable is inside href, src, etc.
Root cause: parseCardsForPreview was called TWICE in generatePreviewHTML:
1. Line 179 - correctly parses markdown to HTML including buttons
2. Line 283 - redundantly called AGAIN after variable highlighting
After first call, variable highlighting (lines 275-280) replaced unknown
variables like {login_url} with <span>[login_url]</span>. When the second
parseCardsForPreview ran, the [login_url] text was misinterpreted as
shortcode syntax, corrupting button HTML output.
Fix: Remove the redundant second call to parseCardsForPreview at line 283.
The function is already called at line 179 before any variable replacement.
1. API Route Fix (NotificationsController.php):
- Changed PUT to POST for /templates/:eventId/:channelId
- Frontend was using api.post() but backend only accepted PUT
- Templates can now be saved
2. Contextual Variables (EventRegistry.php):
- Added get_variables_for_event() method
- Returns category-based variables (order, customer, product, etc.)
- Merges event-specific variables from event definition
- Sorted alphabetically for easy browsing
3. API Response (NotificationsController.php):
- Template API now returns available_variables for the event
- Frontend can show only relevant variables
4. Frontend (EditTemplate.tsx):
- Removed hardcoded 50+ variable list
- Now uses template.available_variables from API
- Variables update based on selected event type
1. Remove wishlist setting from customer settings (now in module toggle)
- Removed from CustomerSettingsProvider.php
- Removed from Customers.tsx
2. Remove auto-login from REST API (causes cookie issues)
- Auto-login in REST context doesn't properly set browser cookies
- Removed wp_set_current_user/wp_set_auth_cookie calls
3. Fix cart not clearing after order
- Added WC()->cart->empty_cart() after successful order
- Server-side cart was not being cleared, causing re-population
- Frontend clears local store but Cart page syncs with server
- Add Campaigns list page with table, status badges, search, actions
- Add Campaign editor with title, subject, content fields
- Add preview modal, test email dialog, send confirmation
- Update Marketing index to show hub with Newsletter, Campaigns, Coupons cards
- Add routes in App.tsx
- Remove inline submenu expansion for Marketing
- Keep it consistent with Appearance and Settings (simple buttons)
- Description provides enough context about what's inside
- Add Marketing section to More page with Newsletter and Coupons submenu
- Remove standalone Coupons entry (now under Marketing)
- Add submenu rendering support for items with children
- Use Megaphone icon for Marketing section
Implements direct-to-cart functionality for landing page CTAs.
Features:
- Parse URL parameters: ?add-to-cart=123
- Support simple products: ?add-to-cart=123
- Support variable products: ?add-to-cart=123&variation_id=456
- Support quantity: ?add-to-cart=123&quantity=2
- Auto-navigate to cart after adding
- Clean URL after adding (remove parameters)
- Toast notification on success/error
Usage examples:
1. Simple product:
https://site.com/store?add-to-cart=332
2. Variable product:
https://site.com/store?add-to-cart=332&variation_id=456
3. With quantity:
https://site.com/store?add-to-cart=332&quantity=3
Flow:
- User clicks CTA on landing page
- Redirects to SPA with add-to-cart parameter
- SPA loads, hook detects parameter
- Adds product to cart via API
- Navigates to cart page
- Shows success toast
Works with both SPA modes:
- Full SPA: loads shop, adds to cart, navigates to cart
- Checkout Only: loads cart, adds to cart, stays on cart
User feedback: 'SPA means Single Page, why 4 pages?'
Correct architecture:
- 1 SPA entry page (e.g., /store)
- SPA Mode determines initial route:
* Full SPA → starts at shop page
* Checkout Only → starts at cart page
* Disabled → never loads
- React Router handles rest via /#/ routing
Changes:
- Admin UI: Changed from 4 page selectors to 1 SPA entry page
- Backend: spa_pages array → spa_page integer
- Template: Initial route based on spa_mode setting
- Simplified is_spa_page() checks (single ID comparison)
Benefits:
- User can set /store as homepage (Settings → Reading)
- Landing page → CTA → direct to cart/checkout
- Clean single entry point
- Mode controls behavior, not multiple pages
Example flow:
- Visit https://site.com/store
- Full SPA: loads shop, navigate via /#/product/123
- Checkout Only: loads cart, navigate via /#/checkout
- Homepage: set /store as homepage, SPA loads on site root
Next: Add direct-to-cart CTA with product parameter
Complete WooCommerce-style page architecture implementation:
Backend (already committed):
- API endpoint to fetch WordPress pages
- spa_pages field in appearance settings
- is_spa_page() checks in TemplateOverride and Assets
Frontend (this commit):
- Added page selector UI in Appearance > General
- Dropdowns for Shop, Cart, Checkout, Account pages
- Loads available WordPress pages from API
- Saves selected page IDs to settings
- Info alert explaining full-body rendering
UI Features:
- Clean page selection interface
- Shows all published WordPress pages
- '— None —' option to disable
- Integrated into existing General settings tab
- Follows existing design patterns
How it works:
1. Admin selects pages in Appearance > General
2. Page IDs saved to woonoow_appearance_settings
3. Frontend checks if current page matches selected pages
4. If match, renders full SPA to body (no theme interference)
5. Works with ANY theme consistently
Next: Test page selection and verify clean SPA rendering
Problem: React warning about missing keys persisted despite keys being present.
Root cause: term_id/attribute_id could be undefined during initial render before API response.
Solution: Add fallback keys using array index when primary ID is undefined:
- Categories: key={category.term_id || `category-${index}`}
- Tags: key={tag.term_id || `tag-${index}`}
- Attributes: key={attribute.attribute_id || `attribute-${index}`}
This ensures React always has a valid key, even during the brief moment
when data is loading or if the API returns malformed data.
Files Modified:
- admin-spa/src/routes/Products/Categories.tsx
- admin-spa/src/routes/Products/Tags.tsx
- admin-spa/src/routes/Products/Attributes.tsx
Result:
✅ React key warnings should be resolved
✅ Graceful handling of edge cases where IDs might be missing
1. Toast Position Control ✅
- Added toast_position setting to Appearance > General
- 6 position options: top-left/center/right, bottom-left/center/right
- Default: top-right
- Backend: AppearanceController.php (save/load toast_position)
- Frontend: Customer SPA reads from appearanceSettings and applies to Toaster
- Admin UI: Select dropdown in General settings
- Solves UX issue: toast blocking cart icon in header
2. Currency Formatting Fix ✅
- Changed formatPrice import from @/lib/utils to @/lib/currency
- @/lib/currency respects WooCommerce currency settings (IDR, not USD)
- Reads currency code, symbol, position, separators from window.woonoowCustomer.currency
- Applies correct formatting for Indonesian Rupiah and any other currency
3. Dialog Accessibility Warnings Fixed ✅
- Added DialogDescription component to all taxonomy dialogs
- Categories: 'Update category information' / 'Create a new product category'
- Tags: 'Update tag information' / 'Create a new product tag'
- Attributes: 'Update attribute information' / 'Create a new product attribute'
- Fixes console warning: Missing Description or aria-describedby
Note on React Key Warning:
The warning about missing keys in ProductCategories is still appearing in console.
All table rows already have proper key props (key={category.term_id}).
This may be a dev server cache issue or a nested element without a key.
The code is correct - keys are present on all mapped elements.
Files Modified:
- includes/Admin/AppearanceController.php (toast_position setting)
- admin-spa/src/routes/Appearance/General.tsx (toast position UI)
- customer-spa/src/App.tsx (apply toast position from settings)
- customer-spa/src/pages/Wishlist.tsx (use correct formatPrice from currency)
- admin-spa/src/routes/Products/Categories.tsx (DialogDescription)
- admin-spa/src/routes/Products/Tags.tsx (DialogDescription)
- admin-spa/src/routes/Products/Attributes.tsx (DialogDescription)
Result:
✅ Toast notifications now configurable and won't block header elements
✅ Prices display in correct currency (IDR) with proper formatting
✅ All Dialog accessibility warnings resolved
⚠️ React key warning persists (but keys are correctly implemented)
Newsletter Fix:
- Move all hooks (useQuery, useMutation) before conditional returns
- Add 'enabled' option to useQuery to control when it fetches
- Fixes React error #310: useEffect called conditionally
- Newsletter page now loads without errors at /marketing/newsletter
Wishlist Module Refactoring:
- Create WishlistSettings.php with 8 configurable settings:
* Enable guest wishlists
* Wishlist page selector
* Show in header toggle
* Enable sharing
* Back in stock notifications
* Max items per wishlist
* Multiple wishlists support
* Show add to cart button
- Add has_settings flag to wishlist module in ModuleRegistry
- Initialize WishlistSettings in woonoow.php
- Update customer-spa BaseLayout to use isEnabled('wishlist') check
- Wishlist page already has module check (no changes needed)
Files Added (1):
- includes/Modules/WishlistSettings.php
Files Modified (5):
- admin-spa/src/routes/Marketing/Newsletter.tsx
- includes/Core/ModuleRegistry.php
- woonoow.php
- customer-spa/src/layouts/BaseLayout.tsx
- admin-spa/dist/app.js (rebuilt)
Both newsletter and wishlist now follow the same module pattern:
- Settings via schema (no code required)
- Module enable/disable controls feature visibility
- Settings page at /settings/modules/{module_id}
- Consistent user experience
- Fix: Marketing events now display in Staff notifications tab
- Reorganize: Move Coupons to Marketing/Coupons for better organization
- Add: Comprehensive email/phone validation with extensible filter hooks
- Email validation with regex pattern (xxxx@xxxx.xx)
- Phone validation with WhatsApp verification support
- Filter hooks for external API integration (QuickEmailVerification, etc.)
- Fix: Newsletter template routes now use centralized notification email builder
- Add: Validation.php class for reusable validation logic
- Add: VALIDATION_HOOKS.md documentation with integration examples
- Add: NEWSLETTER_CAMPAIGN_PLAN.md architecture for future campaign system
- Fix: API delete method call in Newsletter.tsx (delete -> del)
- Remove: Duplicate EmailTemplates.tsx (using notification system instead)
- Update: Newsletter controller to use centralized Validation class
Breaking changes:
- Coupons routes moved from /routes/Coupons to /routes/Marketing/Coupons
- Legacy /coupons routes maintained for backward compatibility
- Add WishlistController with full CRUD API
- Create wishlist page in My Account
- Add heart icon to all product card layouts (always visible)
- Implement useWishlist hook for state management
- Add wishlist toggle in admin Settings > Customer
- Fix wishlist menu visibility based on admin settings
- Fix double navigation in wishlist page
- Fix variable product navigation to use React Router
- Add TypeScript type casting fix for addresses
- Add AddressController with full CRUD API for saved addresses
- Implement address management UI in My Account > Addresses
- Add modal-based address selector in checkout (Tokopedia-style)
- Hide checkout forms when saved address is selected
- Add search functionality in address modal
- Auto-select default addresses on page load
- Fix variable products to show 'Select Options' instead of 'Add to Cart'
- Add admin toggle for multiple addresses feature
- Clean up debug logs and fix TypeScript errors
- Created LayoutWrapper component to conditionally render header/footer based on route
- Created MinimalHeader component (logo only)
- Created MinimalFooter component (trust badges + policy links)
- Created usePageVisibility hook to get visibility settings per page
- Wrapped ClassicLayout with LayoutWrapper for conditional rendering
- Header/footer visibility now controlled directly in React SPA
- Settings: show/minimal/hide for both header and footer
- Background color support for checkout and thankyou pages
Implemented context-aware back button that respects user's navigation path:
Pattern:
```typescript
const handleBack = () => {
if (window.history.state?.idx > 0) {
navigate(-1); // Go back in history
} else {
navigate('/fallback'); // Safe fallback
}
};
```
Updated Pages:
✅ Orders/Detail.tsx → Fallback: /orders
✅ Orders/Edit.tsx → Fallback: /orders/:id
✅ Customers/Detail.tsx → Fallback: /customers
✅ Customers/Edit.tsx → Fallback: /customers
✅ Products/Edit.tsx → Fallback: /products
✅ Coupons/Edit.tsx → Fallback: /coupons
User Flow Examples:
1. Normal Navigation (History Available):
Customers Index → Customer Detail → Orders Tab → Order Detail
→ Click Back → Returns to Customer Detail ✅
2. Direct Access (No History):
User opens /orders/360 directly
→ Click Back → Goes to /orders (fallback) ✅
3. New Tab (No History):
User opens order in new tab
→ Click Back → Goes to /orders (fallback) ✅
4. Page Refresh (History Cleared):
User refreshes page
→ Click Back → Goes to fallback ✅
Benefits:
✅ Respects user's navigation path when possible
✅ Never breaks or leaves the app
✅ Predictable behavior in all scenarios
✅ Professional UX (like Gmail, Shopify, etc.)
✅ Works with deep links and bookmarks
Technical:
- Uses window.history.state.idx to detect history
- Falls back to safe default when no history
- Consistent pattern across all pages
- No URL parameters needed
Result: Back button now works intelligently based on context!