+ {/* Render sections using effective container width */}
+
{sections.map((section) => {
const SectionComponent = SECTION_COMPONENTS[section.type];
diff --git a/docs/_registry.php b/docs/_registry.php
index 1536e23..b70fb7c 100644
--- a/docs/_registry.php
+++ b/docs/_registry.php
@@ -1,4 +1,5 @@
[
- 'label' => 'WooNooW',
+ 'label' => 'Help & Support',
'icon' => 'book-open',
'items' => [
[
'slug' => 'getting-started',
- 'title' => 'Getting Started',
+ 'title' => 'Official Documentation',
'file' => $docs_dir . '/getting-started.md',
],
- [
- 'slug' => 'installation',
- 'title' => 'Installation',
- 'file' => $docs_dir . '/installation.md',
- ],
- [
- 'slug' => 'troubleshooting',
- 'title' => 'Troubleshooting',
- 'file' => $docs_dir . '/troubleshooting.md',
- ],
- [
- 'slug' => 'faq',
- 'title' => 'FAQ',
- 'file' => $docs_dir . '/faq.md',
- ],
- ],
- ],
- 'configuration' => [
- 'label' => 'Configuration',
- 'icon' => 'settings',
- 'items' => [
- [
- 'slug' => 'configuration/appearance',
- 'title' => 'Appearance Settings',
- 'file' => $docs_dir . '/configuration/appearance.md',
- ],
- [
- 'slug' => 'configuration/spa-mode',
- 'title' => 'SPA Mode',
- 'file' => $docs_dir . '/configuration/spa-mode.md',
- ],
- ],
- ],
- 'features' => [
- 'label' => 'Features',
- 'icon' => 'layers',
- 'items' => [
- [
- 'slug' => 'features/shop',
- 'title' => 'Shop Page',
- 'file' => $docs_dir . '/features/shop.md',
- ],
- [
- 'slug' => 'features/checkout',
- 'title' => 'Checkout',
- 'file' => $docs_dir . '/features/checkout.md',
- ],
],
],
];
-
+
/**
* Filter: woonoow_docs_registry
*
@@ -111,9 +66,10 @@ function get_docs_registry() {
* @param string $slug Document slug
* @return array|null Document data with content, or null if not found
*/
-function get_doc_by_slug($slug) {
+function get_doc_by_slug($slug)
+{
$registry = get_docs_registry();
-
+
foreach ($registry as $section) {
foreach ($section['items'] as $item) {
if ($item['slug'] === $slug) {
@@ -127,6 +83,6 @@ function get_doc_by_slug($slug) {
}
}
}
-
+
return null;
}
diff --git a/docs/configuration/appearance.md b/docs/configuration/appearance.md
deleted file mode 100644
index 5357ae5..0000000
--- a/docs/configuration/appearance.md
+++ /dev/null
@@ -1,133 +0,0 @@
-# Appearance Settings
-
-Customize the look and feel of your WooNooW store.
-
-## Accessing Appearance Settings
-
-Go to **WooNooW → Appearance** in the WordPress admin.
-
----
-
-## General Settings
-
-### Logo
-
-Upload your store logo for display in the header.
-
-- **Recommended size**: 200x60 pixels (width x height)
-- **Formats**: PNG (transparent background recommended), SVG, JPG
-- **Mobile**: Automatically resized for smaller screens
-
-### SPA Page
-
-Select which page hosts the WooNooW SPA. Default is "Store".
-
-> **Note**: This page should contain the `[woonoow_spa]` shortcode.
-
-### SPA Mode
-
-Choose how WooNooW handles your store pages:
-
-| Mode | Description |
-|------|-------------|
-| **Full** | All WooCommerce pages redirect to SPA |
-| **Disabled** | Native WooCommerce templates are used |
-
----
-
-## Colors
-
-### Primary Color
-
-The main brand color used for:
-- Buttons
-- Links
-- Active states
-- Primary actions
-
-**Default**: `#6366f1` (Indigo)
-
-### Secondary Color
-
-Secondary UI elements:
-- Less prominent buttons
-- Borders
-- Subtle backgrounds
-
-**Default**: `#64748b` (Slate)
-
-### Accent Color
-
-Highlight color for:
-- Sale badges
-- Notifications
-- Call-to-action elements
-
-**Default**: `#f59e0b` (Amber)
-
----
-
-## Typography
-
-### Body Font
-
-Font used for general text content.
-
-**Options**: System fonts and Google Fonts
-- Inter
-- Open Sans
-- Roboto
-- Lato
-- Poppins
-- And more...
-
-### Heading Font
-
-Font used for titles and headings.
-
-**Options**: Same as body fonts, plus:
-- Cormorant Garamond (Serif option)
-- Playfair Display
-- Merriweather
-
-### Font Sizes
-
-Font sizes are responsive and adjust automatically based on screen size.
-
----
-
-## Layout
-
-### Container Width
-
-Maximum width of the content area.
-
-| Option | Width |
-|--------|-------|
-| Narrow | 1024px |
-| Default | 1280px |
-| Wide | 1536px |
-| Full | 100% |
-
-### Header Style
-
-Configure the header appearance:
-- **Fixed**: Stays at top when scrolling
-- **Static**: Scrolls with page
-
-### Product Grid
-
-Columns in the shop page grid:
-- Mobile: 1-2 columns
-- Tablet: 2-3 columns
-- Desktop: 3-4 columns
-
----
-
-## Saving Changes
-
-1. Make your changes
-2. Click **Save Changes** button
-3. Refresh your store page to see updates
-
-> **Tip**: Open your store in another tab to preview changes quickly.
diff --git a/docs/configuration/spa-mode.md b/docs/configuration/spa-mode.md
deleted file mode 100644
index bbb7608..0000000
--- a/docs/configuration/spa-mode.md
+++ /dev/null
@@ -1,139 +0,0 @@
-# SPA Mode
-
-Understanding and configuring WooNooW's SPA (Single Page Application) mode.
-
-## What is SPA Mode?
-
-SPA Mode controls how WooNooW handles your WooCommerce pages. It determines whether visitors experience the modern SPA interface or traditional WooCommerce templates.
-
----
-
-## Available Modes
-
-### Full Mode (Recommended)
-
-**All WooCommerce pages redirect to the SPA.**
-
-When a visitor navigates to:
-- `/shop` → Redirects to `/store/shop`
-- `/product/example` → Redirects to `/store/product/example`
-- `/cart` → Redirects to `/store/cart`
-- `/checkout` → Redirects to `/store/checkout`
-- `/my-account` → Redirects to `/store/my-account`
-
-**Benefits**:
-- Instant page transitions
-- Modern, consistent UI
-- Better mobile experience
-- Smooth animations
-
-**Best for**:
-- New stores
-- Stores wanting a modern look
-- Mobile-focused businesses
-
-### Disabled Mode
-
-**WooCommerce uses its native templates.**
-
-WooCommerce pages work normally with your theme's templates. WooNooW admin features still work, but the customer-facing SPA is turned off.
-
-**Benefits**:
-- Keep existing theme customizations
-- Compatibility with WooCommerce template overrides
-- Traditional page-by-page navigation
-
-**Best for**:
-- Stores with heavy theme customizations
-- Testing before full rollout
-- Troubleshooting issues
-
----
-
-## Switching Modes
-
-### How to Switch
-
-1. Go to **WooNooW → Appearance → General**
-2. Find **SPA Mode** setting
-3. Select your preferred mode
-4. Click **Save Changes**
-
-### What Happens When Switching
-
-**Switching to Full**:
-- WooCommerce pages start redirecting
-- SPA loads for shop experience
-- No data is changed
-
-**Switching to Disabled**:
-- Redirects stop immediately
-- WooCommerce templates take over
-- No data is changed
-
-> **Note**: All your products, orders, and settings remain unchanged when switching modes.
-
----
-
-## URL Structure
-
-### Full Mode URLs
-
-```
-https://yourstore.com/store/ → Home/Shop
-https://yourstore.com/store/shop → Shop page
-https://yourstore.com/store/product/slug → Product page
-https://yourstore.com/store/cart → Cart
-https://yourstore.com/store/checkout → Checkout
-https://yourstore.com/store/my-account → Account
-```
-
-### Disabled Mode URLs
-
-Standard WooCommerce URLs:
-```
-https://yourstore.com/shop/ → Shop page
-https://yourstore.com/product/slug → Product page
-https://yourstore.com/cart/ → Cart
-https://yourstore.com/checkout/ → Checkout
-https://yourstore.com/my-account/ → Account
-```
-
----
-
-## SEO Considerations
-
-### Full Mode SEO
-
-- WooCommerce URLs (`/product/slug`) remain in sitemaps
-- When users click from search results, they're redirected to SPA
-- Meta tags are generated dynamically for social sharing
-- 302 (temporary) redirects preserve link equity
-
-### Disabled Mode SEO
-
-- Standard WooCommerce SEO applies
-- No redirects needed
-- Works with Yoast SEO, RankMath, etc.
-
----
-
-## Troubleshooting
-
-### Redirects Not Working
-
-1. **Flush Permalinks**: Go to Settings → Permalinks → Save Changes
-2. **Check Store Page**: Ensure the Store page exists and has `[woonoow_spa]`
-3. **Clear Cache**: Purge all caching layers
-
-### Blank Pages After Enabling
-
-1. Verify SPA Mode is set to "Full"
-2. Clear browser cache
-3. Check for JavaScript errors in browser console
-
-### Want to Test Before Enabling
-
-1. Keep mode as "Disabled"
-2. Visit `/store/` directly to preview SPA
-3. Switch to "Full" when satisfied
diff --git a/docs/faq.md b/docs/faq.md
deleted file mode 100644
index facb93a..0000000
--- a/docs/faq.md
+++ /dev/null
@@ -1,149 +0,0 @@
-# Frequently Asked Questions
-
-Quick answers to common questions about WooNooW.
-
----
-
-## General
-
-### What is WooNooW?
-
-WooNooW is a WooCommerce plugin that transforms your store into a modern Single Page Application (SPA). It provides instant page loads, a beautiful UI, and seamless shopping experience.
-
-### Do I need WooCommerce?
-
-Yes. WooNooW is an enhancement layer for WooCommerce. You need WooCommerce installed and activated.
-
-### Will WooNooW affect my existing products?
-
-No. WooNooW reads from WooCommerce. Your products, orders, and settings remain untouched.
-
----
-
-## SPA Mode
-
-### What's the difference between Full and Disabled mode?
-
-| Mode | Behavior |
-|------|----------|
-| **Full** | All WooCommerce pages redirect to SPA. Modern, fast experience. |
-| **Disabled** | WooCommerce pages use native templates. WooNooW admin still works. |
-
-### Can I switch modes anytime?
-
-Yes. Go to **WooNooW → Appearance → General** and change the SPA Mode. Changes take effect immediately.
-
-### Which mode should I use?
-
-- **Full**: For the best customer experience with instant loads
-- **Disabled**: If you have theme customizations you want to keep
-
----
-
-## Compatibility
-
-### Does WooNooW work with my theme?
-
-WooNooW's SPA is independent of your WordPress theme. In Full mode, the SPA uses its own styling. Your theme affects the rest of your site normally.
-
-### Does WooNooW work with page builders?
-
-The SPA pages are self-contained. Page builders work on other pages of your site.
-
-### Which payment gateways are supported?
-
-WooNooW supports all WooCommerce-compatible payment gateways:
-- PayPal
-- Stripe
-- Bank Transfer (BACS)
-- Cash on Delivery
-- And more...
-
----
-
-## SEO
-
-### Is WooNooW SEO-friendly?
-
-Yes. WooNooW uses:
-- Clean URLs (`/store/product/product-name`)
-- Dynamic meta tags for social sharing
-- Proper redirects (302) from WooCommerce URLs
-
-### What about my existing SEO?
-
-WooCommerce URLs remain the indexed source. WooNooW redirects users to the SPA but preserves SEO value.
-
-### Will my product pages be indexed?
-
-Yes. Search engines index the WooCommerce URLs. When users click from search results, they're redirected to the fast SPA experience.
-
----
-
-## Performance
-
-### Is WooNooW faster than regular WooCommerce?
-
-Yes, for navigation. After the initial load, page transitions are instant because the SPA doesn't reload the entire page.
-
-### Will WooNooW slow down my site?
-
-The initial load is similar to regular WooCommerce. Subsequent navigation is much faster.
-
-### Does WooNooW work with caching?
-
-Yes. Use page caching and object caching for best results.
-
----
-
-## Customization
-
-### Can I customize colors and fonts?
-
-Yes. Go to **WooNooW → Appearance** to customize:
-- Primary, secondary, and accent colors
-- Body and heading fonts
-- Logo and layout options
-
-### Can I add custom CSS?
-
-Currently, use your theme's Additional CSS feature. A custom CSS field may be added in future versions.
-
-### Can I modify the SPA templates?
-
-The SPA is built with React. Advanced customizations require development knowledge.
-
----
-
-## Addons
-
-### What are WooNooW addons?
-
-Addons extend WooNooW with additional features like loyalty points, advanced analytics, etc.
-
-### How do I install addons?
-
-Addons are installed as separate WordPress plugins. They integrate automatically with WooNooW.
-
-### Do addons work when SPA is disabled?
-
-Most addon features are for the SPA. When disabled, addon functionality may be limited.
-
----
-
-## Troubleshooting
-
-### I see a blank page. What do I do?
-
-1. Check SPA Mode is set to "Full"
-2. Flush permalinks (**Settings → Permalinks → Save**)
-3. Clear all caches
-4. See [Troubleshooting](troubleshooting) for more
-
-### How do I report a bug?
-
-Contact support with:
-- Steps to reproduce the issue
-- WordPress/WooCommerce/WooNooW versions
-- Any error messages
-- Screenshots if applicable
diff --git a/docs/features/checkout.md b/docs/features/checkout.md
deleted file mode 100644
index fd9863d..0000000
--- a/docs/features/checkout.md
+++ /dev/null
@@ -1,145 +0,0 @@
-# Checkout
-
-The WooNooW checkout provides a streamlined purchasing experience.
-
-## Overview
-
-The checkout process includes:
-
-1. **Cart Review** - Verify items before checkout
-2. **Customer Information** - Billing and shipping details
-3. **Payment Method** - Select how to pay
-4. **Order Confirmation** - Complete the purchase
-
----
-
-## Checkout Flow
-
-### Step 1: Cart
-
-Before checkout, customers review their cart:
-- Product list with images
-- Quantity adjustments
-- Remove items
-- Apply coupon codes
-- See subtotal, shipping, and total
-
-### Step 2: Customer Details
-
-Customers provide:
-- **Email address**
-- **Billing information**
- - Name
- - Address
- - Phone
-- **Shipping address** (if different from billing)
-
-> **Note**: Logged-in customers have their details pre-filled.
-
-### Step 3: Shipping Method
-
-If physical products are in the cart:
-- Available shipping methods are shown
-- Shipping cost is calculated
-- Customer selects preferred method
-
-### Step 4: Payment
-
-Customers choose their payment method:
-- Credit/Debit Card (Stripe, PayPal, etc.)
-- Bank Transfer
-- Cash on Delivery
-- Other configured gateways
-
-### Step 5: Place Order
-
-After reviewing everything:
-- Click "Place Order"
-- Payment is processed
-- Confirmation page is shown
-- Email receipt is sent
-
----
-
-## Features
-
-### Guest Checkout
-
-Allow customers to checkout without creating an account.
-
-Configure in **WooCommerce → Settings → Accounts & Privacy**.
-
-### Coupon Codes
-
-Customers can apply discount codes:
-1. Enter code in the coupon field
-2. Click "Apply"
-3. Discount is reflected in total
-
-### Order Notes
-
-Optional field for customers to add special instructions.
-
----
-
-## Payment Gateways
-
-### Supported Gateways
-
-WooNooW supports all WooCommerce payment gateways:
-
-| Gateway | Type |
-|---------|------|
-| Bank Transfer (BACS) | Manual |
-| Check Payments | Manual |
-| Cash on Delivery | Manual |
-| PayPal | Card / PayPal |
-| Stripe | Card |
-| Square | Card |
-
-### Configuring Gateways
-
-1. Go to **WooNooW → Settings → Payments**
-2. Enable desired payment methods
-3. Configure API keys and settings
-4. Test with sandbox/test mode first
-
----
-
-## After Checkout
-
-### Order Confirmation Page
-
-Shows:
-- Order number
-- Order summary
-- Next steps
-
-### Confirmation Email
-
-Automatically sent to customer with:
-- Order details
-- Payment confirmation
-- Shipping information (if applicable)
-
----
-
-## Troubleshooting
-
-### "Place Order" Button Not Working
-
-1. Check all required fields are filled
-2. Verify payment gateway is properly configured
-3. Check browser console for JavaScript errors
-
-### Payment Declined
-
-1. Customer should verify card details
-2. Check payment gateway dashboard for error details
-3. Ensure correct API keys are configured
-
-### Shipping Not Showing
-
-1. Verify shipping zones are configured in WooCommerce
-2. Check if products have weight/dimensions set
-3. Confirm customer's address is in a configured zone
diff --git a/docs/features/shop.md b/docs/features/shop.md
deleted file mode 100644
index 3a749eb..0000000
--- a/docs/features/shop.md
+++ /dev/null
@@ -1,96 +0,0 @@
-# Shop Page
-
-The shop page displays your product catalog with browsing and filtering options.
-
-## Overview
-
-The WooNooW shop page provides:
-
-- **Product Grid** - Visual display of products
-- **Search** - Find products by name
-- **Filters** - Category and sorting options
-- **Pagination** - Navigate through products
-
----
-
-## Features
-
-### Product Cards
-
-Each product displays:
-- Product image
-- Product name
-- Price (with sale price if applicable)
-- Add to Cart button
-- Wishlist button (if enabled)
-
-### Search
-
-Type in the search box to filter products by name. Search is instant and updates the grid as you type.
-
-### Category Filter
-
-Filter products by category using the dropdown. Shows:
-- All Categories
-- Individual categories with product count
-
-### Sorting
-
-Sort products by:
-- Default sorting
-- Popularity
-- Average rating
-- Latest
-- Price: Low to High
-- Price: High to Low
-
----
-
-## Customization
-
-### Grid Layout
-
-Configure the product grid in **WooNooW → Appearance**:
-
-| Device | Options |
-|--------|---------|
-| Mobile | 1-2 columns |
-| Tablet | 2-4 columns |
-| Desktop | 2-6 columns |
-
-### Product Card Style
-
-Product cards can display:
-- **Image** - Product featured image
-- **Title** - Product name
-- **Price** - Current price and sale price
-- **Rating** - Star rating (if reviews enabled)
-- **Add to Cart** - Quick add button
-
----
-
-## Navigation
-
-### Clicking a Product
-
-Clicking a product card navigates to the full product page where customers can:
-- View all images
-- Select variations
-- Read description
-- Add to cart
-
-### Back to Shop
-
-From any product page, use the breadcrumb or browser back button to return to the shop.
-
----
-
-## Performance
-
-### Lazy Loading
-
-Product images load as they come into view, improving initial page load time.
-
-### Infinite Scroll vs Pagination
-
-Currently uses pagination. Infinite scroll may be added in future versions.
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 9a558af..176951d 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -1,54 +1,28 @@
-# Getting Started with WooNooW
+# Help Center
-Welcome to WooNooW! This guide will help you get up and running quickly.
+Welcome to the WooNooW documentation hub. Here you can find comprehensive guides, resources, and support to elevate your WooCommerce store.
-## What is WooNooW?
+## 📚 Official Documentation
-WooNooW transforms your WooCommerce store into a modern, fast Single Page Application (SPA). It provides:
+We have moved our documentation to a dedicated site for a better reading experience.
-- ⚡ **Instant Page Loads** - No page refreshes between navigation
-- 🎨 **Modern UI** - Beautiful, responsive design out of the box
-- 🛠 **Easy Customization** - Configure colors, fonts, and layout from admin
-- 📱 **Mobile-First** - Optimized for all devices
+[**Visit docs.woonoow.com ↗**](https://docs.woonoow.com)
-## Quick Setup (3 Steps)
+---
-### Step 1: Activate the Plugin
+## 🚀 Quick Links
-After installing WooNooW, activate it from **Plugins → Installed Plugins**.
+| Section | Description |
+| :--- | :--- |
+| **[Getting Started](https://docs.woonoow.com/docs/getting-started)** | Installation, Requirements, and Quick Start guides. |
+| **[Configuration](https://docs.woonoow.com/docs/configuration)** | Learn how to customize SPA appearance and plugin settings. |
+| **[Notifications](https://docs.woonoow.com/docs/hooks/notifications)** | Setup email templates, SMS, and WhatsApp channels. |
+| **[Developer API](https://docs.woonoow.com/docs/developer)** | Hooks, Filters, and Addon development guides. |
-The plugin will automatically:
-- Create a "Store" page for the SPA
-- Configure basic settings
+## 🛠 Support & Resources
-### Step 2: Access Admin Dashboard
+- **[Support Ticket](https://woonoow.com/support)**: Get help from our engineering team.
+- **[System Status](https://status.woonoow.com)**: Check API and service availability.
+- **[Changelog](https://docs.woonoow.com/docs/changelog)**: See what's new in the latest version.
-Go to **WooNooW** in your WordPress admin menu.
-
-You'll see the admin dashboard with:
-- Orders management
-- Settings configuration
-- Appearance customization
-
-### Step 3: Configure Your Store
-
-Navigate to **Appearance** settings to:
-
-1. **Upload your logo**
-2. **Set brand colors** (primary, secondary, accent)
-3. **Choose fonts** for headings and body text
-4. **Configure SPA mode** (Full or Disabled)
-
-## Next Steps
-
-- [Installation Guide](installation) - Detailed installation instructions
-- [Appearance Settings](configuration/appearance) - Customize your store's look
-- [SPA Mode](configuration/spa-mode) - Understand Full vs Disabled mode
-- [Troubleshooting](troubleshooting) - Common issues and solutions
-
-## Need Help?
-
-If you encounter any issues:
-1. Check the [Troubleshooting](troubleshooting) guide
-2. Review the [FAQ](faq)
-3. Contact support with your WordPress and WooCommerce versions
+> **Tip:** Ensure your license is active in **Settings > License** to receive automatic updates and premium support.
diff --git a/docs/installation.md b/docs/installation.md
deleted file mode 100644
index 411eb01..0000000
--- a/docs/installation.md
+++ /dev/null
@@ -1,92 +0,0 @@
-# Installation Guide
-
-This guide covers installing WooNooW on your WordPress site.
-
-## Requirements
-
-Before installing, ensure your site meets these requirements:
-
-| Requirement | Minimum | Recommended |
-|-------------|---------|-------------|
-| WordPress | 6.0+ | Latest |
-| WooCommerce | 7.0+ | Latest |
-| PHP | 7.4+ | 8.1+ |
-| MySQL | 5.7+ | 8.0+ |
-
-## Installation Methods
-
-### Method 1: WordPress Admin (Recommended)
-
-1. Go to **Plugins → Add New**
-2. Click **Upload Plugin**
-3. Select the `woonoow.zip` file
-4. Click **Install Now**
-5. Click **Activate**
-
-### Method 2: FTP Upload
-
-1. Extract `woonoow.zip` to get the `woonoow` folder
-2. Upload to `/wp-content/plugins/`
-3. Go to **Plugins → Installed Plugins**
-4. Find WooNooW and click **Activate**
-
-## Post-Installation
-
-After activation, WooNooW automatically:
-
-### 1. Creates Store Page
-A new "Store" page is created with the SPA shortcode. This is your main storefront.
-
-### 2. Registers Rewrite Rules
-URL routes like `/store/shop` and `/store/product/...` are registered.
-
-> **Note**: If you see 404 errors, go to **Settings → Permalinks** and click **Save Changes** to flush rewrite rules.
-
-### 3. Sets Default Configuration
-Basic appearance settings are configured with sensible defaults.
-
-## Verification Checklist
-
-After installation, verify everything works:
-
-- [ ] Plugin activated without errors
-- [ ] WooNooW menu appears in admin sidebar
-- [ ] Store page exists (check **Pages**)
-- [ ] `/store` URL loads the SPA
-- [ ] Products display on shop page
-
-## WooCommerce Compatibility
-
-WooNooW works alongside WooCommerce:
-
-| WooCommerce Page | WooNooW Behavior (Full Mode) |
-|------------------|------------------------------|
-| `/shop` | Redirects to `/store/shop` |
-| `/product/...` | Redirects to `/store/product/...` |
-| `/cart` | Redirects to `/store/cart` |
-| `/checkout` | Redirects to `/store/checkout` |
-| `/my-account` | Redirects to `/store/my-account` |
-
-When SPA Mode is **Disabled**, WooCommerce pages work normally.
-
-## Updating
-
-To update WooNooW:
-
-1. Download the latest version
-2. Go to **Plugins → Installed Plugins**
-3. Deactivate WooNooW (optional but recommended)
-4. Delete the old version
-5. Install and activate the new version
-
-Your settings are preserved in the database.
-
-## Uninstalling
-
-To completely remove WooNooW:
-
-1. Deactivate the plugin (restores WooCommerce page content)
-2. Delete the plugin
-3. (Optional) Delete WooNooW options from database
-
-> **Note**: Deactivating restores original WooCommerce shortcodes to Cart, Checkout, and My Account pages.
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
deleted file mode 100644
index cfff489..0000000
--- a/docs/troubleshooting.md
+++ /dev/null
@@ -1,173 +0,0 @@
-# Troubleshooting
-
-Common issues and their solutions.
-
-## Blank Pages
-
-### Symptom
-WooCommerce pages (shop, cart, checkout) show blank content.
-
-### Solutions
-
-**1. Check SPA Mode Setting**
-- Go to **WooNooW → Appearance → General**
-- Ensure **SPA Mode** is set to "Full"
-- If you want native WooCommerce, set to "Disabled"
-
-**2. Flush Permalinks**
-- Go to **Settings → Permalinks**
-- Click **Save Changes** (no changes needed)
-- This refreshes rewrite rules
-
-**3. Clear Cache**
-If using a caching plugin:
-- Clear page cache
-- Clear object cache
-- Purge CDN cache (if applicable)
-
----
-
-## 404 Errors on SPA Routes
-
-### Symptom
-Visiting `/store/shop` or `/store/product/...` shows a 404 error.
-
-### Solutions
-
-**1. Flush Permalinks**
-- Go to **Settings → Permalinks**
-- Click **Save Changes**
-
-**2. Check Store Page Exists**
-- Go to **Pages**
-- Verify "Store" page exists and is published
-- The page should contain `[woonoow_spa]` shortcode
-
-**3. Check SPA Page Setting**
-- Go to **WooNooW → Appearance → General**
-- Ensure **SPA Page** is set to the Store page
-
----
-
-## Product Images Not Loading
-
-### Symptom
-Products show placeholder images instead of actual images.
-
-### Solutions
-
-**1. Regenerate Thumbnails**
-- Install "Regenerate Thumbnails" plugin
-- Run regeneration for all images
-
-**2. Check Image URLs**
-- Ensure images have valid URLs
-- Check for mixed content (HTTP vs HTTPS)
-
----
-
-## Slow Performance
-
-### Symptom
-SPA feels slow or laggy.
-
-### Solutions
-
-**1. Enable Caching**
-- Install a caching plugin (WP Super Cache, W3 Total Cache)
-- Enable object caching (Redis/Memcached)
-
-**2. Optimize Images**
-- Use WebP format
-- Compress images before upload
-- Use lazy loading
-
-**3. Check Server Resources**
-- Upgrade hosting if on shared hosting
-- Consider VPS or managed WordPress hosting
-
----
-
-## Checkout Not Working
-
-### Symptom
-Checkout page won't load or payment fails.
-
-### Solutions
-
-**1. Check Payment Gateway**
-- Go to **WooCommerce → Settings → Payments**
-- Verify payment method is enabled
-- Check API credentials
-
-**2. Check SSL Certificate**
-- Checkout requires HTTPS
-- Verify SSL is properly installed
-
-**3. Check for JavaScript Errors**
-- Open browser Developer Tools (F12)
-- Check Console for errors
-- Look for blocked scripts
-
----
-
-## Emails Not Sending
-
-### Symptom
-Order confirmation emails not being received.
-
-### Solutions
-
-**1. Check Email Settings**
-- Go to **WooNooW → Settings → Notifications**
-- Verify email types are enabled
-
-**2. Check WordPress Email**
-- Test with a plugin like "Check & Log Email"
-- Consider using SMTP plugin (WP Mail SMTP)
-
-**3. Check Spam Folder**
-- Emails may be in recipient's spam folder
-- Add sender to whitelist
-
----
-
-## Plugin Conflicts
-
-### Symptom
-WooNooW doesn't work after installing another plugin.
-
-### Steps to Diagnose
-
-1. **Deactivate other plugins** one by one
-2. **Switch to default theme** (Twenty Twenty-Three)
-3. **Check error logs** in `wp-content/debug.log`
-
-### Common Conflicting Plugins
-
-- Other WooCommerce template overrides
-- Page builder plugins (sometimes)
-- Heavy caching plugins (misconfigured)
-
----
-
-## Getting More Help
-
-If you can't resolve the issue:
-
-1. **Collect Information**
- - WordPress version
- - WooCommerce version
- - WooNooW version
- - PHP version
- - Error messages (from debug.log)
-
-2. **Enable Debug Mode**
- Add to `wp-config.php`:
- ```php
- define('WP_DEBUG', true);
- define('WP_DEBUG_LOG', true);
- ```
-
-3. **Contact Support**
- Provide the collected information for faster resolution.
diff --git a/includes/Admin/AppearanceController.php b/includes/Admin/AppearanceController.php
index 1ef75fd..509de81 100644
--- a/includes/Admin/AppearanceController.php
+++ b/includes/Admin/AppearanceController.php
@@ -1,41 +1,45 @@
'GET',
'callback' => [__CLASS__, 'get_settings'],
'permission_callback' => '__return_true',
]);
-
+
// Save general settings
register_rest_route(self::API_NAMESPACE, '/appearance/general', [
'methods' => 'POST',
'callback' => [__CLASS__, 'save_general'],
'permission_callback' => [__CLASS__, 'check_permission'],
]);
-
+
// Save header settings
register_rest_route(self::API_NAMESPACE, '/appearance/header', [
'methods' => 'POST',
'callback' => [__CLASS__, 'save_header'],
'permission_callback' => [__CLASS__, 'check_permission'],
]);
-
+
// Save footer settings
register_rest_route(self::API_NAMESPACE, '/appearance/footer', [
'methods' => 'POST',
@@ -49,7 +53,7 @@ class AppearanceController {
'callback' => [__CLASS__, 'save_menus'],
'permission_callback' => [__CLASS__, 'check_permission'],
]);
-
+
// Save page-specific settings
register_rest_route(self::API_NAMESPACE, '/appearance/pages/(?P
[a-zA-Z0-9_-]+)', [
'methods' => 'POST',
@@ -63,7 +67,7 @@ class AppearanceController {
],
],
]);
-
+
// Get all WordPress pages for page selector
register_rest_route(self::API_NAMESPACE, '/pages/list', [
'methods' => 'GET',
@@ -71,40 +75,45 @@ class AppearanceController {
'permission_callback' => [__CLASS__, 'check_permission'],
]);
}
-
- public static function check_permission() {
+
+ public static function check_permission()
+ {
return current_user_can('manage_woocommerce');
}
-
+
/**
* Get all appearance settings
*/
- public static function get_settings(WP_REST_Request $request) {
+ public static function get_settings(WP_REST_Request $request)
+ {
$stored = get_option(self::OPTION_KEY, []);
$defaults = self::get_default_settings();
-
+
// Merge stored with defaults to ensure all fields exist (recursive)
$settings = array_replace_recursive($defaults, $stored);
-
+
return new WP_REST_Response([
'success' => true,
'data' => $settings,
], 200);
}
-
+
/**
* Save general settings
*/
- public static function save_general(WP_REST_Request $request) {
+ public static function save_general(WP_REST_Request $request)
+ {
$settings = get_option(self::OPTION_KEY, []);
$defaults = self::get_default_settings();
$settings = array_replace_recursive($defaults, $settings);
-
+
$colors = $request->get_param('colors') ?? [];
$general_data = [
'spa_mode' => sanitize_text_field($request->get_param('spaMode')),
'spa_page' => absint($request->get_param('spaPage') ?? 0),
+
+ 'container_width' => sanitize_text_field($request->get_param('containerWidth') ?? 'boxed'),
'toast_position' => sanitize_text_field($request->get_param('toastPosition') ?? 'top-right'),
'typography' => [
'mode' => sanitize_text_field($request->get_param('typography')['mode'] ?? 'predefined'),
@@ -125,23 +134,25 @@ class AppearanceController {
'gradientEnd' => sanitize_hex_color($colors['gradientEnd'] ?? '#3b82f6'),
],
];
-
- $settings['general'] = $general_data;
+
+ // Merge with existing general settings to preserve other keys (like spa_frontpage)
+ $settings['general'] = array_merge($settings['general'] ?? [], $general_data);
update_option(self::OPTION_KEY, $settings);
-
+
return new WP_REST_Response([
'success' => true,
'message' => 'General settings saved successfully',
'data' => $general_data,
], 200);
}
-
+
/**
* Save header settings
*/
- public static function save_header(WP_REST_Request $request) {
+ public static function save_header(WP_REST_Request $request)
+ {
$settings = get_option(self::OPTION_KEY, self::get_default_settings());
-
+
$header_data = [
'style' => sanitize_text_field($request->get_param('style')),
'sticky' => (bool) $request->get_param('sticky'),
@@ -159,23 +170,24 @@ class AppearanceController {
'wishlist' => (bool) ($request->get_param('elements')['wishlist'] ?? false),
],
];
-
+
$settings['header'] = $header_data;
update_option(self::OPTION_KEY, $settings);
-
+
return new WP_REST_Response([
'success' => true,
'message' => 'Header settings saved successfully',
'data' => $header_data,
], 200);
}
-
+
/**
* Save footer settings
*/
- public static function save_footer(WP_REST_Request $request) {
+ public static function save_footer(WP_REST_Request $request)
+ {
$settings = get_option(self::OPTION_KEY, self::get_default_settings());
-
+
$social_links = $request->get_param('socialLinks') ?? [];
$sanitized_links = [];
foreach ($social_links as $link) {
@@ -185,7 +197,7 @@ class AppearanceController {
'url' => esc_url_raw($link['url'] ?? ''),
];
}
-
+
// Sanitize contact data
$contact_data = $request->get_param('contactData');
$sanitized_contact = [
@@ -196,7 +208,7 @@ class AppearanceController {
'show_phone' => (bool) ($contact_data['show_phone'] ?? true),
'show_address' => (bool) ($contact_data['show_address'] ?? true),
];
-
+
// Sanitize labels
$labels = $request->get_param('labels');
$sanitized_labels = [
@@ -206,7 +218,7 @@ class AppearanceController {
'newsletter_title' => sanitize_text_field($labels['newsletter_title'] ?? 'Newsletter'),
'newsletter_description' => sanitize_text_field($labels['newsletter_description'] ?? 'Subscribe to get updates'),
];
-
+
// Sanitize custom sections
$sections = $request->get_param('sections') ?? [];
$sanitized_sections = [];
@@ -219,28 +231,44 @@ class AppearanceController {
'visible' => (bool) ($section['visible'] ?? true),
];
}
-
+
$footer_data = [
+ 'payment' => [
+ 'enabled' => (bool) ($request->get_param('payment')['enabled'] ?? true),
+ 'title' => sanitize_text_field($request->get_param('payment')['title'] ?? 'We accept'),
+ 'methods' => array_map(function ($method) {
+ return [
+ 'id' => sanitize_text_field($method['id'] ?? uniqid()),
+ 'url' => esc_url_raw($method['url'] ?? ''),
+ 'label' => sanitize_text_field($method['label'] ?? ''),
+ 'width' => sanitize_text_field($method['width'] ?? ''),
+ ];
+ }, $request->get_param('payment')['methods'] ?? []),
+ ],
+ 'copyright' => [
+ 'enabled' => (bool) ($request->get_param('copyright')['enabled'] ?? true),
+ 'text' => wp_kses_post($request->get_param('copyright')['text'] ?? ''),
+ ],
'columns' => sanitize_text_field($request->get_param('columns')),
'style' => sanitize_text_field($request->get_param('style')),
- 'copyright_text' => wp_kses_post($request->get_param('copyrightText')),
+ // 'copyright_text' => Deprecated, moved to copyright.text
+ // 'elements' => Deprecated, moved to individual sections (except menu/contact/social flags if needed)
'elements' => [
'newsletter' => (bool) ($request->get_param('elements')['newsletter'] ?? true),
'social' => (bool) ($request->get_param('elements')['social'] ?? true),
- 'payment' => (bool) ($request->get_param('elements')['payment'] ?? true),
- 'copyright' => (bool) ($request->get_param('elements')['copyright'] ?? true),
'menu' => (bool) ($request->get_param('elements')['menu'] ?? true),
'contact' => (bool) ($request->get_param('elements')['contact'] ?? true),
+ // Payment and Copyright moved
],
'social_links' => $sanitized_links,
'contact_data' => $sanitized_contact,
'labels' => $sanitized_labels,
'sections' => $sanitized_sections,
];
-
+
$settings['footer'] = $footer_data;
update_option(self::OPTION_KEY, $settings);
-
+
return new WP_REST_Response([
'success' => true,
'message' => 'Footer settings saved successfully',
@@ -251,11 +279,12 @@ class AppearanceController {
/**
* Save menu settings
*/
- public static function save_menus(WP_REST_Request $request) {
+ public static function save_menus(WP_REST_Request $request)
+ {
$settings = get_option(self::OPTION_KEY, self::get_default_settings());
-
+
$menus = $request->get_param('menus') ?? [];
-
+
// Sanitize menus
$sanitized_menus = [
'primary' => [],
@@ -265,7 +294,7 @@ class AppearanceController {
foreach (['primary', 'mobile'] as $location) {
if (isset($menus[$location]) && is_array($menus[$location])) {
foreach ($menus[$location] as $item) {
- $sanitized_menus[$location][] = [
+ $sanitized_menus[$location][] = [
'id' => sanitize_text_field($item['id'] ?? uniqid()),
'label' => sanitize_text_field($item['label'] ?? ''),
'type' => sanitize_text_field($item['type'] ?? 'page'), // page, custom
@@ -275,46 +304,48 @@ class AppearanceController {
}
}
}
-
+
$settings['menus'] = $sanitized_menus;
update_option(self::OPTION_KEY, $settings);
-
+
return new WP_REST_Response([
'success' => true,
'message' => 'Menu settings saved successfully',
'data' => $sanitized_menus,
], 200);
}
-
+
/**
* Save page-specific settings
*/
- public static function save_page_settings(WP_REST_Request $request) {
+ public static function save_page_settings(WP_REST_Request $request)
+ {
$settings = get_option(self::OPTION_KEY, self::get_default_settings());
$page = $request->get_param('page');
-
+
// Get all parameters from request
$page_data = $request->get_json_params();
-
+
// Sanitize based on page type
$sanitized_data = self::sanitize_page_data($page, $page_data);
-
+
$settings['pages'][$page] = $sanitized_data;
update_option(self::OPTION_KEY, $settings);
-
+
return new WP_REST_Response([
'success' => true,
'message' => ucfirst($page) . ' page settings saved successfully',
'data' => $sanitized_data,
], 200);
}
-
+
/**
* Sanitize page-specific data
*/
- private static function sanitize_page_data($page, $data) {
+ private static function sanitize_page_data($page, $data)
+ {
$sanitized = [];
-
+
switch ($page) {
case 'shop':
$sanitized = [
@@ -342,7 +373,7 @@ class AppearanceController {
],
];
break;
-
+
case 'product':
$sanitized = [
'layout' => [
@@ -366,7 +397,7 @@ class AppearanceController {
],
];
break;
-
+
case 'cart':
$sanitized = [
'layout' => [
@@ -381,7 +412,7 @@ class AppearanceController {
],
];
break;
-
+
case 'checkout':
$sanitized = [
'layout' => [
@@ -399,7 +430,7 @@ class AppearanceController {
],
];
break;
-
+
case 'thankyou':
$sanitized = [
'template' => sanitize_text_field($data['template'] ?? 'basic'),
@@ -414,7 +445,7 @@ class AppearanceController {
],
];
break;
-
+
case 'account':
$sanitized = [
'layout' => [
@@ -430,31 +461,32 @@ class AppearanceController {
];
break;
}
-
+
return $sanitized;
}
-
+
/**
* Get list of WordPress pages for page selector
*/
- public static function get_pages_list(WP_REST_Request $request) {
+ public static function get_pages_list(WP_REST_Request $request)
+ {
$pages = get_pages([
'post_status' => 'publish',
'sort_column' => 'post_title',
'sort_order' => 'ASC',
]);
-
+
$store_pages = [
(int) get_option('woocommerce_shop_page_id'),
(int) get_option('woocommerce_cart_page_id'),
(int) get_option('woocommerce_checkout_page_id'),
(int) get_option('woocommerce_myaccount_page_id'),
];
-
- $pages_list = array_map(function($page) use ($store_pages) {
+
+ $pages_list = array_map(function ($page) use ($store_pages) {
$is_woonoow = !empty(get_post_meta($page->ID, '_wn_page_structure', true));
$is_store = in_array((int)$page->ID, $store_pages, true);
-
+
return [
'id' => $page->ID,
'title' => $page->post_title,
@@ -463,21 +495,24 @@ class AppearanceController {
'is_store_page' => $is_store,
];
}, $pages);
-
+
return new WP_REST_Response([
'success' => true,
'data' => $pages_list,
], 200);
}
-
+
/**
* Get default settings structure
*/
- public static function get_default_settings() {
+ public static function get_default_settings()
+ {
return [
'general' => [
'spa_mode' => 'full',
'spa_page' => 0,
+
+ 'container_width' => 'boxed',
'toast_position' => 'top-right',
'typography' => [
'mode' => 'predefined',
@@ -516,12 +551,22 @@ class AppearanceController {
'footer' => [
'columns' => '4',
'style' => 'detailed',
- 'copyright_text' => '© 2024 WooNooW. All rights reserved.',
+ 'payment' => [
+ 'enabled' => true,
+ 'title' => 'We accept',
+ 'methods' => [
+ ['id' => 'visa', 'url' => '', 'label' => 'Visa', 'default_icon' => 'visa'], // Placeholder logic for defaults
+ ['id' => 'mastercard', 'url' => '', 'label' => 'Mastercard', 'default_icon' => 'mastercard'],
+ ['id' => 'paypal', 'url' => '', 'label' => 'PayPal', 'default_icon' => 'paypal'],
+ ],
+ ],
+ 'copyright' => [
+ 'enabled' => true,
+ 'text' => '© 2024 WooNooW. All rights reserved.',
+ ],
'elements' => [
'newsletter' => true,
'social' => true,
- 'payment' => true,
- 'copyright' => true,
'menu' => true,
'contact' => true,
],
diff --git a/includes/Api/CampaignsController.php b/includes/Api/CampaignsController.php
index ed4c93a..4df27a3 100644
--- a/includes/Api/CampaignsController.php
+++ b/includes/Api/CampaignsController.php
@@ -1,4 +1,5 @@
'GET',
'callback' => [__CLASS__, 'get_campaigns'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
-
+
// Create campaign
register_rest_route(self::API_NAMESPACE, '/campaigns', [
'methods' => 'POST',
'callback' => [__CLASS__, 'create_campaign'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
-
+
// Get single campaign
register_rest_route(self::API_NAMESPACE, '/campaigns/(?P\d+)', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_campaign'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
-
+
// Update campaign
register_rest_route(self::API_NAMESPACE, '/campaigns/(?P\d+)', [
'methods' => 'PUT',
'callback' => [__CLASS__, 'update_campaign'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
-
+
// Delete campaign
register_rest_route(self::API_NAMESPACE, '/campaigns/(?P\d+)', [
'methods' => 'DELETE',
'callback' => [__CLASS__, 'delete_campaign'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
-
+
// Send campaign
register_rest_route(self::API_NAMESPACE, '/campaigns/(?P\d+)/send', [
'methods' => 'POST',
'callback' => [__CLASS__, 'send_campaign'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
-
+
// Send test email
register_rest_route(self::API_NAMESPACE, '/campaigns/(?P\d+)/test', [
'methods' => 'POST',
'callback' => [__CLASS__, 'send_test_email'],
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
-
+
// Preview campaign
register_rest_route(self::API_NAMESPACE, '/campaigns/(?P\d+)/preview', [
'methods' => 'GET',
@@ -78,30 +82,33 @@ class CampaignsController {
'permission_callback' => [__CLASS__, 'check_admin_permission'],
]);
}
-
+
/**
* Check admin permission
*/
- public static function check_admin_permission() {
+ public static function check_admin_permission()
+ {
return current_user_can('manage_options');
}
-
+
/**
* Get all campaigns
*/
- public static function get_campaigns(WP_REST_Request $request) {
+ public static function get_campaigns(WP_REST_Request $request)
+ {
$campaigns = CampaignManager::get_all();
-
+
return new WP_REST_Response([
'success' => true,
'data' => $campaigns,
]);
}
-
+
/**
* Create campaign
*/
- public static function create_campaign(WP_REST_Request $request) {
+ public static function create_campaign(WP_REST_Request $request)
+ {
$data = [
'title' => $request->get_param('title'),
'subject' => $request->get_param('subject'),
@@ -109,52 +116,54 @@ class CampaignsController {
'status' => $request->get_param('status') ?: 'draft',
'scheduled_at' => $request->get_param('scheduled_at'),
];
-
+
$campaign_id = CampaignManager::create($data);
-
+
if (is_wp_error($campaign_id)) {
return new WP_REST_Response([
'success' => false,
'error' => $campaign_id->get_error_message(),
], 400);
}
-
+
$campaign = CampaignManager::get($campaign_id);
-
+
return new WP_REST_Response([
'success' => true,
'data' => $campaign,
], 201);
}
-
+
/**
* Get single campaign
*/
- public static function get_campaign(WP_REST_Request $request) {
+ public static function get_campaign(WP_REST_Request $request)
+ {
$campaign_id = (int) $request->get_param('id');
$campaign = CampaignManager::get($campaign_id);
-
+
if (!$campaign) {
return new WP_REST_Response([
'success' => false,
'error' => __('Campaign not found', 'woonoow'),
], 404);
}
-
+
return new WP_REST_Response([
'success' => true,
'data' => $campaign,
]);
}
-
+
/**
* Update campaign
*/
- public static function update_campaign(WP_REST_Request $request) {
+ public static function update_campaign(WP_REST_Request $request)
+ {
$campaign_id = (int) $request->get_param('id');
-
+
$data = [];
-
+
if ($request->has_param('title')) {
$data['title'] = $request->get_param('title');
}
@@ -170,60 +179,62 @@ class CampaignsController {
if ($request->has_param('scheduled_at')) {
$data['scheduled_at'] = $request->get_param('scheduled_at');
}
-
+
$result = CampaignManager::update($campaign_id, $data);
-
+
if (is_wp_error($result)) {
return new WP_REST_Response([
'success' => false,
'error' => $result->get_error_message(),
], 400);
}
-
+
$campaign = CampaignManager::get($campaign_id);
-
+
return new WP_REST_Response([
'success' => true,
'data' => $campaign,
]);
}
-
+
/**
* Delete campaign
*/
- public static function delete_campaign(WP_REST_Request $request) {
+ public static function delete_campaign(WP_REST_Request $request)
+ {
$campaign_id = (int) $request->get_param('id');
-
+
$result = CampaignManager::delete($campaign_id);
-
+
if (!$result) {
return new WP_REST_Response([
'success' => false,
'error' => __('Failed to delete campaign', 'woonoow'),
], 400);
}
-
+
return new WP_REST_Response([
'success' => true,
'message' => __('Campaign deleted', 'woonoow'),
]);
}
-
+
/**
* Send campaign
*/
- public static function send_campaign(WP_REST_Request $request) {
+ public static function send_campaign(WP_REST_Request $request)
+ {
$campaign_id = (int) $request->get_param('id');
-
+
$result = CampaignManager::send($campaign_id);
-
+
if (!$result['success']) {
return new WP_REST_Response([
'success' => false,
'error' => $result['error'],
], 400);
}
-
+
return new WP_REST_Response([
'success' => true,
'message' => sprintf(
@@ -236,63 +247,80 @@ class CampaignsController {
'total' => $result['total'],
]);
}
-
+
/**
* Send test email
*/
- public static function send_test_email(WP_REST_Request $request) {
+ public static function send_test_email(WP_REST_Request $request)
+ {
$campaign_id = (int) $request->get_param('id');
$email = sanitize_email($request->get_param('email'));
-
+
if (!is_email($email)) {
return new WP_REST_Response([
'success' => false,
'error' => __('Invalid email address', 'woonoow'),
], 400);
}
-
+
$result = CampaignManager::send_test($campaign_id, $email);
-
+
if (!$result) {
return new WP_REST_Response([
'success' => false,
'error' => __('Failed to send test email', 'woonoow'),
], 400);
}
-
+
+ // Log to activity log
+ Logger::log(
+ 'test_sent',
+ 'campaign',
+ $campaign_id,
+ sprintf(__('Test email sent to %s', 'woonoow'), $email)
+ );
+
return new WP_REST_Response([
'success' => true,
'message' => sprintf(__('Test email sent to %s', 'woonoow'), $email),
]);
}
-
+
/**
* Preview campaign
*/
- public static function preview_campaign(WP_REST_Request $request) {
+ public static function preview_campaign(WP_REST_Request $request)
+ {
$campaign_id = (int) $request->get_param('id');
$campaign = CampaignManager::get($campaign_id);
-
+
if (!$campaign) {
return new WP_REST_Response([
'success' => false,
'error' => __('Campaign not found', 'woonoow'),
], 404);
}
-
+
// Use reflection to call private render method or make it public
// For now, return a simple preview
$renderer = \WooNooW\Core\Notifications\EmailRenderer::instance();
$template = $renderer->get_template_settings('newsletter_campaign', 'customer');
-
+
$content = $campaign['content'];
$subject = $campaign['subject'] ?: $campaign['title'];
-
+
if ($template) {
+ // Use template subject if available
+ if (!empty($template['subject'])) {
+ $subject = $template['subject'];
+ }
$content = str_replace('{content}', $campaign['content'], $template['body']);
$content = str_replace('{campaign_title}', $campaign['title'], $content);
}
-
+
+ // Replace campaign_title in subject
+ $subject = str_replace('{campaign_title}', $campaign['title'], $subject);
+
// Replace placeholders
$site_name = get_bloginfo('name');
$content = str_replace(['{site_name}', '{store_name}'], $site_name, $content);
@@ -301,7 +329,10 @@ class CampaignsController {
$content = str_replace('{unsubscribe_url}', '#unsubscribe', $content);
$content = str_replace('{current_date}', date_i18n(get_option('date_format')), $content);
$content = str_replace('{current_year}', date('Y'), $content);
-
+
+ // Parse card shortcodes before rendering
+ $content = $renderer->parse_cards($content);
+
// Render with design template
$design_path = $renderer->get_design_template();
if (file_exists($design_path)) {
@@ -310,7 +341,7 @@ class CampaignsController {
'site_url' => home_url(),
]);
}
-
+
return new WP_REST_Response([
'success' => true,
'subject' => $subject,
diff --git a/includes/Api/CheckoutController.php b/includes/Api/CheckoutController.php
index 143c740..3189c81 100644
--- a/includes/Api/CheckoutController.php
+++ b/includes/Api/CheckoutController.php
@@ -311,12 +311,27 @@ class CheckoutController
return ['error' => __('No items provided', 'woonoow')];
}
+ // Security: Rate limiting check
+ if (\WooNooW\Compat\SecuritySettingsProvider::is_rate_limited()) {
+ return ['error' => __('Too many orders. Please try again later.', 'woonoow')];
+ }
+
+ // Security: CAPTCHA validation
+ $captcha_token = $payload['captcha_token'] ?? '';
+ $captcha_result = \WooNooW\Compat\SecuritySettingsProvider::validate_captcha($captcha_token);
+ if (is_wp_error($captcha_result)) {
+ return ['error' => $captcha_result->get_error_message()];
+ }
+
// Create order
$order = wc_create_order();
if (is_wp_error($order)) {
return ['error' => $order->get_error_message()];
}
+ // Track if user was logged in during this request (for frontend page reload)
+ $user_logged_in = false;
+
// Set customer ID if user is logged in
if (is_user_logged_in()) {
$user_id = get_current_user_id();
@@ -358,8 +373,9 @@ class CheckoutController
$existing_user = get_user_by('email', $email);
if ($existing_user) {
- // User exists - link order to them
+ // User exists - link order to them (but do NOT auto-login for security)
$order->set_customer_id($existing_user->ID);
+ // Note: user_logged_in stays false - existing users must authenticate separately
} else {
// Create new user account
$password = wp_generate_password(12, true, true);
@@ -387,6 +403,7 @@ class CheckoutController
// AUTO-LOGIN: Set authentication cookie so user is logged in after page reload
wp_set_auth_cookie($new_user_id, true);
wp_set_current_user($new_user_id);
+ $user_logged_in = true;
// Set WooCommerce customer billing data
$customer = new \WC_Customer($new_user_id);
@@ -509,6 +526,9 @@ class CheckoutController
WC()->cart->empty_cart();
}
+ // Record this order attempt for rate limiting
+ \WooNooW\Compat\SecuritySettingsProvider::record_order_attempt();
+
return [
'ok' => true,
'order_id' => $order->get_id(),
@@ -516,6 +536,7 @@ class CheckoutController
'status' => $order->get_status(),
'pay_url' => $order->get_checkout_payment_url(),
'thankyou_url' => $order->get_checkout_order_received_url(),
+ 'user_logged_in' => $user_logged_in, // True if user was logged in during this request (requires page reload)
];
}
diff --git a/includes/Api/NewsletterController.php b/includes/Api/NewsletterController.php
index 15ba841..5dfbe3a 100644
--- a/includes/Api/NewsletterController.php
+++ b/includes/Api/NewsletterController.php
@@ -1,15 +1,19 @@
'POST',
'callback' => [__CLASS__, 'subscribe'],
@@ -18,45 +22,45 @@ class NewsletterController {
'email' => [
'required' => true,
'type' => 'string',
- 'validate_callback' => function($param) {
+ 'validate_callback' => function ($param) {
return is_email($param);
},
],
],
]);
-
+
register_rest_route(self::API_NAMESPACE, '/newsletter/subscribers', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_subscribers'],
- 'permission_callback' => function() {
+ 'permission_callback' => function () {
return current_user_can('manage_options');
},
]);
-
+
register_rest_route(self::API_NAMESPACE, '/newsletter/subscribers/(?P[^/]+)', [
'methods' => 'DELETE',
'callback' => [__CLASS__, 'delete_subscriber'],
- 'permission_callback' => function() {
+ 'permission_callback' => function () {
return current_user_can('manage_options');
},
]);
-
+
register_rest_route(self::API_NAMESPACE, '/newsletter/template/(?P[^/]+)', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_template'],
- 'permission_callback' => function() {
+ 'permission_callback' => function () {
return current_user_can('manage_options');
},
]);
-
+
register_rest_route(self::API_NAMESPACE, '/newsletter/template/(?P[^/]+)', [
'methods' => 'POST',
'callback' => [__CLASS__, 'save_template'],
- 'permission_callback' => function() {
+ 'permission_callback' => function () {
return current_user_can('manage_options');
},
]);
-
+
// Public unsubscribe endpoint (no auth needed, uses token)
register_rest_route(self::API_NAMESPACE, '/newsletter/unsubscribe', [
'methods' => 'GET',
@@ -73,139 +77,381 @@ class NewsletterController {
],
],
]);
+
+ // Public confirm endpoint (double opt-in)
+ register_rest_route(self::API_NAMESPACE, '/newsletter/confirm', [
+ 'methods' => 'GET',
+ 'callback' => [__CLASS__, 'confirm'],
+ 'permission_callback' => '__return_true',
+ 'args' => [
+ 'email' => [
+ 'required' => true,
+ 'type' => 'string',
+ ],
+ 'token' => [
+ 'required' => true,
+ 'type' => 'string',
+ ],
+ ],
+ ]);
}
-
- public static function get_template(WP_REST_Request $request) {
+
+ public static function get_template(WP_REST_Request $request)
+ {
$template = $request->get_param('template');
$option_key = "woonoow_newsletter_{$template}_template";
-
+
$data = get_option($option_key, [
'subject' => $template === 'welcome' ? 'Welcome to {site_name} Newsletter!' : 'Confirm your newsletter subscription',
- 'content' => $template === 'welcome'
+ 'content' => $template === 'welcome'
? "Thank you for subscribing to our newsletter!\n\nYou'll receive updates about our latest products and offers.\n\nBest regards,\n{site_name}"
: "Please confirm your newsletter subscription by clicking the link below:\n\n{confirmation_url}\n\nBest regards,\n{site_name}",
]);
-
+
return new WP_REST_Response([
'success' => true,
'subject' => $data['subject'] ?? '',
'content' => $data['content'] ?? '',
], 200);
}
-
- public static function save_template(WP_REST_Request $request) {
+
+ public static function save_template(WP_REST_Request $request)
+ {
$template = $request->get_param('template');
$subject = sanitize_text_field($request->get_param('subject'));
$content = wp_kses_post($request->get_param('content'));
-
+
$option_key = "woonoow_newsletter_{$template}_template";
-
+
update_option($option_key, [
'subject' => $subject,
'content' => $content,
]);
-
+
return new WP_REST_Response([
'success' => true,
'message' => 'Template saved successfully',
], 200);
}
-
- public static function delete_subscriber(WP_REST_Request $request) {
- $email = urldecode($request->get_param('email'));
+
+ public static function delete_subscriber(WP_REST_Request $request)
+ {
+ $email = sanitize_email(urldecode($request->get_param('email')));
+
+ if (self::use_custom_table()) {
+ $result = SubscriberTable::delete_by_email($email);
+ if ($result) {
+ return new WP_REST_Response([
+ 'success' => true,
+ 'message' => 'Subscriber removed successfully',
+ ], 200);
+ }
+ return new WP_Error('not_found', 'Subscriber not found', ['status' => 404]);
+ }
+
+ // Legacy: wp_options storage
$subscribers = get_option('woonoow_newsletter_subscribers', []);
-
- $subscribers = array_filter($subscribers, function($sub) use ($email) {
+
+ $subscribers = array_filter($subscribers, function ($sub) use ($email) {
return isset($sub['email']) && $sub['email'] !== $email;
});
-
+
update_option('woonoow_newsletter_subscribers', array_values($subscribers));
-
+
return new WP_REST_Response([
'success' => true,
'message' => 'Subscriber removed successfully',
], 200);
}
-
- public static function subscribe(WP_REST_Request $request) {
+
+ /**
+ * Check if custom subscriber table should be used
+ */
+ private static function use_custom_table()
+ {
+ return SubscriberTable::table_exists();
+ }
+
+ public static function subscribe(WP_REST_Request $request)
+ {
$email = sanitize_email($request->get_param('email'));
-
+ $consent = (bool) $request->get_param('consent');
+
+ // Rate limiting (5 requests per IP per hour)
+ $ip = $_SERVER['REMOTE_ADDR'] ?? '';
+ $rate_key = 'woonoow_newsletter_rate_' . md5($ip);
+ $attempts = (int) get_transient($rate_key);
+
+ if ($attempts >= 5) {
+ return new WP_Error('rate_limited', __('Too many requests. Please try again later.', 'woonoow'), ['status' => 429]);
+ }
+ set_transient($rate_key, $attempts + 1, HOUR_IN_SECONDS);
+
// Use centralized validation with extensible filter hooks
$validation = Validation::validate_email($email, 'newsletter_subscribe');
-
+
if (is_wp_error($validation)) {
return $validation;
}
-
- // Get existing subscribers (now stored as objects with metadata)
- $subscribers = get_option('woonoow_newsletter_subscribers', []);
-
- // Check if already subscribed
- $existing = array_filter($subscribers, function($sub) use ($email) {
- return isset($sub['email']) && $sub['email'] === $email;
- });
-
- if (!empty($existing)) {
- return new WP_REST_Response([
- 'success' => true,
- 'message' => 'You are already subscribed to our newsletter!',
- ], 200);
+
+ // Check GDPR consent requirement
+ $gdpr_required = get_option('woonoow_newsletter_gdpr_consent', false);
+ if ($gdpr_required && !$consent) {
+ return new WP_Error('consent_required', __('Please accept the terms to subscribe.', 'woonoow'), ['status' => 400]);
}
-
+
// Check if email belongs to a WP user
$user = get_user_by('email', $email);
$user_id = $user ? $user->ID : null;
-
- // Add new subscriber with metadata
- $subscribers[] = [
- 'email' => $email,
- 'user_id' => $user_id,
- 'status' => 'active',
- 'subscribed_at' => current_time('mysql'),
- 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '',
- ];
-
- update_option('woonoow_newsletter_subscribers', $subscribers);
-
- // Trigger notification events
+
+ // Check double opt-in setting
+ $double_opt_in = get_option('woonoow_newsletter_double_opt_in', true);
+ $status = $double_opt_in ? 'pending' : 'active';
+
+ if (self::use_custom_table()) {
+ // Use custom table
+ $existing = SubscriberTable::get_by_email($email);
+
+ if ($existing) {
+ if ($existing['status'] === 'active') {
+ return new WP_REST_Response([
+ 'success' => true,
+ 'message' => __('You are already subscribed to our newsletter!', 'woonoow'),
+ ], 200);
+ }
+ if ($existing['status'] === 'pending') {
+ self::send_confirmation_email($email, $existing['user_id'] ?? null);
+ return new WP_REST_Response([
+ 'success' => true,
+ 'message' => __('Confirmation email resent. Please check your inbox.', 'woonoow'),
+ ], 200);
+ }
+ // Resubscribe (was unsubscribed)
+ SubscriberTable::update_by_email($email, [
+ 'status' => $status,
+ 'consent' => $consent ? 1 : 0,
+ 'subscribed_at' => current_time('mysql'),
+ 'ip_address' => $ip,
+ ]);
+ } else {
+ // New subscriber
+ SubscriberTable::add([
+ 'email' => $email,
+ 'user_id' => $user_id,
+ 'status' => $status,
+ 'consent' => $consent,
+ 'subscribed_at' => current_time('mysql'),
+ 'ip_address' => $ip,
+ ]);
+ }
+ } else {
+ // Legacy: wp_options storage
+ $subscribers = get_option('woonoow_newsletter_subscribers', []);
+
+ // Check if already subscribed
+ $existing_key = null;
+ foreach ($subscribers as $key => $sub) {
+ if (isset($sub['email']) && $sub['email'] === $email) {
+ $existing_key = $key;
+ break;
+ }
+ }
+
+ if ($existing_key !== null) {
+ $existing = $subscribers[$existing_key];
+ if (($existing['status'] ?? 'active') === 'active') {
+ return new WP_REST_Response([
+ 'success' => true,
+ 'message' => __('You are already subscribed to our newsletter!', 'woonoow'),
+ ], 200);
+ }
+ if (($existing['status'] ?? '') === 'pending') {
+ self::send_confirmation_email($email, $existing['user_id'] ?? null);
+ return new WP_REST_Response([
+ 'success' => true,
+ 'message' => __('Confirmation email resent. Please check your inbox.', 'woonoow'),
+ ], 200);
+ }
+ }
+
+ $subscribers[] = [
+ 'email' => $email,
+ 'user_id' => $user_id,
+ 'status' => $status,
+ 'consent' => $consent,
+ 'subscribed_at' => current_time('mysql'),
+ 'ip_address' => $ip,
+ ];
+
+ update_option('woonoow_newsletter_subscribers', $subscribers);
+ }
+
+ if ($double_opt_in) {
+ // Send confirmation email
+ self::send_confirmation_email($email, $user_id);
+
+ return new WP_REST_Response([
+ 'success' => true,
+ 'message' => __('Please check your email to confirm your subscription.', 'woonoow'),
+ ], 200);
+ }
+
+ // Direct subscription (no double opt-in)
do_action('woonoow_newsletter_subscribed', $email, $user_id);
-
- // Trigger notification system events (uses email builder)
+
do_action('woonoow/notification/event', 'newsletter_welcome', 'customer', [
'email' => $email,
'user_id' => $user_id,
'subscribed_at' => current_time('mysql'),
]);
-
+
do_action('woonoow/notification/event', 'newsletter_subscribed_admin', 'staff', [
'email' => $email,
'user_id' => $user_id,
'subscribed_at' => current_time('mysql'),
]);
-
+
return new WP_REST_Response([
'success' => true,
- 'message' => 'Successfully subscribed! Check your email for confirmation.',
+ 'message' => __('Successfully subscribed to our newsletter!', 'woonoow'),
], 200);
}
-
- private static function send_welcome_email($email) {
- $site_name = get_bloginfo('name');
- $template = get_option('woonoow_newsletter_welcome_template', '');
-
- if (empty($template)) {
- $template = "Thank you for subscribing to our newsletter!\n\nYou'll receive updates about our latest products and offers.\n\nBest regards,\n{site_name}";
- }
-
- $subject = sprintf('Welcome to %s Newsletter!', $site_name);
- $message = str_replace('{site_name}', $site_name, $template);
-
- wp_mail($email, $subject, $message);
+
+ /**
+ * Send confirmation email for double opt-in
+ */
+ private static function send_confirmation_email($email, $user_id = null)
+ {
+ $confirmation_url = self::generate_confirmation_url($email);
+
+ do_action('woonoow/notification/event', 'newsletter_confirm', 'customer', [
+ 'email' => $email,
+ 'user_id' => $user_id,
+ 'confirmation_url' => $confirmation_url,
+ ]);
}
-
- public static function get_subscribers(WP_REST_Request $request) {
+
+ /**
+ * Generate confirmation URL with secure token
+ */
+ public static function generate_confirmation_url($email)
+ {
+ $token = self::generate_unsubscribe_token($email); // Reuse same token logic
+ $base_url = rest_url('woonoow/v1/newsletter/confirm');
+ return add_query_arg([
+ 'email' => urlencode($email),
+ 'token' => $token,
+ ], $base_url);
+ }
+
+ /**
+ * Handle confirmation request (double opt-in)
+ */
+ public static function confirm(WP_REST_Request $request)
+ {
+ $email = sanitize_email(urldecode($request->get_param('email')));
+ $token = sanitize_text_field($request->get_param('token'));
+
+ // Verify token
+ $expected_token = self::generate_unsubscribe_token($email);
+ if (!hash_equals($expected_token, $token)) {
+ return new WP_REST_Response([
+ 'success' => false,
+ 'message' => __('Invalid confirmation link', 'woonoow'),
+ ], 400);
+ }
+
+ $found = false;
+ $user_id = null;
+
+ if (self::use_custom_table()) {
+ $existing = SubscriberTable::get_by_email($email);
+ if ($existing) {
+ if ($existing['status'] === 'active') {
+ $found = true;
+ } else {
+ SubscriberTable::update_by_email($email, [
+ 'status' => 'active',
+ 'confirmed_at' => current_time('mysql'),
+ ]);
+ $user_id = $existing['user_id'] ?? null;
+ $found = true;
+ }
+ }
+ } else {
+ // Legacy: wp_options
+ $subscribers = get_option('woonoow_newsletter_subscribers', []);
+
+ foreach ($subscribers as &$sub) {
+ if (isset($sub['email']) && $sub['email'] === $email) {
+ if (($sub['status'] ?? '') === 'active') {
+ $found = true;
+ break;
+ }
+ $sub['status'] = 'active';
+ $sub['confirmed_at'] = current_time('mysql');
+ $user_id = $sub['user_id'] ?? null;
+ $found = true;
+ break;
+ }
+ }
+
+ if ($found) {
+ update_option('woonoow_newsletter_subscribers', $subscribers);
+ }
+ }
+
+ // Trigger subscription events
+ do_action('woonoow_newsletter_subscribed', $email, $user_id);
+
+ do_action('woonoow/notification/event', 'newsletter_welcome', 'customer', [
+ 'email' => $email,
+ 'user_id' => $user_id,
+ 'subscribed_at' => current_time('mysql'),
+ ]);
+
+ do_action('woonoow/notification/event', 'newsletter_subscribed_admin', 'staff', [
+ 'email' => $email,
+ 'user_id' => $user_id,
+ 'subscribed_at' => current_time('mysql'),
+ ]);
+
+ // Return HTML page for nice UX
+ $site_name = get_bloginfo('name');
+ $shop_url = wc_get_page_permalink('shop') ?: home_url();
+ $html = sprintf(
+ '%s',
+ __('Subscription Confirmed', 'woonoow'),
+ esc_html($site_name),
+ esc_url($shop_url)
+ );
+
+ header('Content-Type: text/html; charset=utf-8');
+ echo $html;
+ exit;
+ }
+
+ // Dead code removed: send_welcome_email() - now handled via notification system
+
+ public static function get_subscribers(WP_REST_Request $request)
+ {
+ if (self::use_custom_table()) {
+ $result = SubscriberTable::get_all([
+ 'per_page' => 100,
+ 'page' => 1,
+ ]);
+ return new WP_REST_Response([
+ 'success' => true,
+ 'data' => [
+ 'subscribers' => $result['items'],
+ 'count' => $result['total'],
+ ],
+ ], 200);
+ }
+
+ // Legacy: wp_options
$subscribers = get_option('woonoow_newsletter_subscribers', []);
-
+
return new WP_REST_Response([
'success' => true,
'data' => [
@@ -214,14 +460,15 @@ class NewsletterController {
],
], 200);
}
-
+
/**
* Handle unsubscribe request
*/
- public static function unsubscribe(WP_REST_Request $request) {
+ public static function unsubscribe(WP_REST_Request $request)
+ {
$email = sanitize_email(urldecode($request->get_param('email')));
$token = sanitize_text_field($request->get_param('token'));
-
+
// Verify token
$expected_token = self::generate_unsubscribe_token($email);
if (!hash_equals($expected_token, $token)) {
@@ -230,31 +477,45 @@ class NewsletterController {
'message' => __('Invalid unsubscribe link', 'woonoow'),
], 400);
}
-
- // Get subscribers
- $subscribers = get_option('woonoow_newsletter_subscribers', []);
+
$found = false;
-
- foreach ($subscribers as &$sub) {
- if (isset($sub['email']) && $sub['email'] === $email) {
- $sub['status'] = 'unsubscribed';
- $sub['unsubscribed_at'] = current_time('mysql');
+
+ if (self::use_custom_table()) {
+ $existing = SubscriberTable::get_by_email($email);
+ if ($existing) {
+ SubscriberTable::update_by_email($email, [
+ 'status' => 'unsubscribed',
+ 'unsubscribed_at' => current_time('mysql'),
+ ]);
$found = true;
- break;
+ }
+ } else {
+ // Legacy: wp_options
+ $subscribers = get_option('woonoow_newsletter_subscribers', []);
+
+ foreach ($subscribers as &$sub) {
+ if (isset($sub['email']) && $sub['email'] === $email) {
+ $sub['status'] = 'unsubscribed';
+ $sub['unsubscribed_at'] = current_time('mysql');
+ $found = true;
+ break;
+ }
+ }
+
+ if ($found) {
+ update_option('woonoow_newsletter_subscribers', $subscribers);
}
}
-
+
if (!$found) {
return new WP_REST_Response([
'success' => false,
'message' => __('Email not found', 'woonoow'),
], 404);
}
-
- update_option('woonoow_newsletter_subscribers', $subscribers);
-
+
do_action('woonoow_newsletter_unsubscribed', $email);
-
+
// Return HTML page for nice UX
$site_name = get_bloginfo('name');
$html = sprintf(
@@ -262,24 +523,26 @@ class NewsletterController {
__('Unsubscribed', 'woonoow'),
esc_html($site_name)
);
-
+
header('Content-Type: text/html; charset=utf-8');
echo $html;
exit;
}
-
+
/**
* Generate secure unsubscribe token
*/
- private static function generate_unsubscribe_token($email) {
+ private static function generate_unsubscribe_token($email)
+ {
$secret = wp_salt('auth');
return hash_hmac('sha256', $email, $secret);
}
-
+
/**
* Generate unsubscribe URL for email templates
*/
- public static function generate_unsubscribe_url($email) {
+ public static function generate_unsubscribe_url($email)
+ {
$token = self::generate_unsubscribe_token($email);
$base_url = rest_url('woonoow/v1/newsletter/unsubscribe');
return add_query_arg([
@@ -288,4 +551,3 @@ class NewsletterController {
], $base_url);
}
}
-
diff --git a/includes/Api/NotificationsController.php b/includes/Api/NotificationsController.php
index 5536603..5f77489 100644
--- a/includes/Api/NotificationsController.php
+++ b/includes/Api/NotificationsController.php
@@ -1,4 +1,5 @@
namespace, '/' . $this->rest_base . '/channels', [
[
@@ -41,7 +44,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// GET /woonoow/v1/notifications/events
register_rest_route($this->namespace, '/' . $this->rest_base . '/events', [
[
@@ -50,7 +53,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// POST /woonoow/v1/notifications/events/update
register_rest_route($this->namespace, '/' . $this->rest_base . '/events/update', [
[
@@ -59,7 +62,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// GET /woonoow/v1/notifications/templates
register_rest_route($this->namespace, '/' . $this->rest_base . '/templates', [
[
@@ -68,7 +71,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// GET/POST /woonoow/v1/notifications/templates/:eventId/:channelId
register_rest_route($this->namespace, '/' . $this->rest_base . '/templates/(?P[a-zA-Z0-9_-]+)/(?P[a-zA-Z0-9_-]+)', [
[
@@ -82,7 +85,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// POST /woonoow/v1/notifications/templates
register_rest_route($this->namespace, '/' . $this->rest_base . '/templates', [
[
@@ -91,7 +94,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// DELETE /woonoow/v1/notifications/templates/:eventId/:channelId
register_rest_route($this->namespace, '/' . $this->rest_base . '/templates/(?P[a-zA-Z0-9_-]+)/(?P[a-zA-Z0-9_-]+)', [
[
@@ -100,7 +103,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// GET /woonoow/v1/notifications/push/vapid-key
register_rest_route($this->namespace, '/' . $this->rest_base . '/push/vapid-key', [
[
@@ -109,7 +112,7 @@ class NotificationsController {
'permission_callback' => '__return_true',
],
]);
-
+
// POST /woonoow/v1/notifications/push/subscribe
register_rest_route($this->namespace, '/' . $this->rest_base . '/push/subscribe', [
[
@@ -118,7 +121,7 @@ class NotificationsController {
'permission_callback' => '__return_true',
],
]);
-
+
// POST /woonoow/v1/notifications/push/unsubscribe
register_rest_route($this->namespace, '/' . $this->rest_base . '/push/unsubscribe', [
[
@@ -127,7 +130,7 @@ class NotificationsController {
'permission_callback' => '__return_true',
],
]);
-
+
// GET /woonoow/v1/notifications/email-settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/email-settings', [
[
@@ -136,7 +139,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// POST /woonoow/v1/notifications/email-settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/email-settings', [
[
@@ -145,7 +148,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// DELETE /woonoow/v1/notifications/email-settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/email-settings', [
[
@@ -154,7 +157,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// GET /woonoow/v1/notifications/push/settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/push/settings', [
[
@@ -163,7 +166,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// POST /woonoow/v1/notifications/push/settings
register_rest_route($this->namespace, '/' . $this->rest_base . '/push/settings', [
[
@@ -172,7 +175,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// POST /woonoow/v1/notifications/channels/toggle
register_rest_route($this->namespace, '/' . $this->rest_base . '/channels/toggle', [
[
@@ -181,7 +184,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// GET /woonoow/v1/notifications/staff/events
register_rest_route($this->namespace, '/' . $this->rest_base . '/staff/events', [
[
@@ -190,7 +193,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// GET /woonoow/v1/notifications/customer/events
register_rest_route($this->namespace, '/' . $this->rest_base . '/customer/events', [
[
@@ -199,7 +202,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// GET /woonoow/v1/notifications/system-mode
register_rest_route($this->namespace, '/' . $this->rest_base . '/system-mode', [
[
@@ -208,7 +211,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// POST /woonoow/v1/notifications/system-mode
register_rest_route($this->namespace, '/' . $this->rest_base . '/system-mode', [
[
@@ -217,7 +220,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// GET /woonoow/v1/notifications/logs
register_rest_route($this->namespace, '/' . $this->rest_base . '/logs', [
[
@@ -226,7 +229,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'],
],
]);
-
+
// POST /woonoow/v1/notifications/templates/:eventId/:channelId/send-test
register_rest_route($this->namespace, '/' . $this->rest_base . '/templates/(?P[a-zA-Z0-9_-]+)/(?P[a-zA-Z0-9_-]+)/send-test', [
[
@@ -247,18 +250,19 @@ class NotificationsController {
],
]);
}
-
+
/**
* Get available notification channels
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response
*/
- public function get_channels(WP_REST_Request $request) {
+ public function get_channels(WP_REST_Request $request)
+ {
// Get channel enabled states
$email_enabled = get_option('woonoow_email_notifications_enabled', true);
$push_enabled = get_option('woonoow_push_notifications_enabled', true);
-
+
$channels = [
[
'id' => 'email',
@@ -275,25 +279,26 @@ class NotificationsController {
'builtin' => true,
],
];
-
+
// Allow addons to register their channels
$channels = apply_filters('woonoow_notification_channels', $channels);
-
+
return new WP_REST_Response($channels, 200);
}
-
+
/**
* Get notification events
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response
*/
- public function get_events(WP_REST_Request $request) {
+ public function get_events(WP_REST_Request $request)
+ {
$settings = get_option('woonoow_notification_settings', []);
-
+
// Get all events from EventRegistry (single source of truth)
$all_events = EventRegistry::get_all_events();
-
+
// Group by category and add settings
$grouped_events = [];
foreach ($all_events as $event) {
@@ -301,7 +306,7 @@ class NotificationsController {
if (!isset($grouped_events[$category])) {
$grouped_events[$category] = [];
}
-
+
// Add channels from settings
$event_id = $event['id'];
$event['channels'] = $settings[$event_id]['channels'] ?? [
@@ -309,73 +314,76 @@ class NotificationsController {
'push' => ['enabled' => false, 'recipient' => $event['recipient_type']]
];
$event['recipients'] = [$event['recipient_type']];
-
+
$grouped_events[$category][] = $event;
}
-
+
return new WP_REST_Response($grouped_events, 200);
}
-
+
/**
* Get staff notification events (admin/staff recipient)
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response
*/
- public function get_staff_events(WP_REST_Request $request) {
+ public function get_staff_events(WP_REST_Request $request)
+ {
$all_events = $this->get_all_events();
-
+
// Filter events where recipient_type is 'staff'
$staff_events = [];
foreach ($all_events as $category => $events) {
- $filtered = array_filter($events, function($event) {
+ $filtered = array_filter($events, function ($event) {
return ($event['recipient_type'] ?? 'staff') === 'staff';
});
-
+
if (!empty($filtered)) {
$staff_events[$category] = array_values($filtered);
}
}
-
+
return new WP_REST_Response($staff_events, 200);
}
-
+
/**
* Get customer notification events (customer recipient)
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response
*/
- public function get_customer_events(WP_REST_Request $request) {
+ public function get_customer_events(WP_REST_Request $request)
+ {
$all_events = $this->get_all_events();
-
+
// Filter events where recipient_type is 'customer'
$customer_events = [];
foreach ($all_events as $category => $events) {
- $filtered = array_filter($events, function($event) {
+ $filtered = array_filter($events, function ($event) {
return ($event['recipient_type'] ?? 'staff') === 'customer';
});
-
+
if (!empty($filtered)) {
$customer_events[$category] = array_values($filtered);
}
}
-
+
return new WP_REST_Response($customer_events, 200);
}
-
+
/**
* Get all events (internal helper)
*
* @return array
*/
- private function get_all_events() {
+ private function get_all_events()
+ {
// Use EventRegistry - same as get_events() but returns ungrouped
$settings = get_option('woonoow_notification_settings', []);
-
+
// Get all events from EventRegistry (single source of truth)
$all_events = EventRegistry::get_all_events();
-
+
// Group by category and add settings
$grouped_events = [];
foreach ($all_events as $event) {
@@ -383,33 +391,34 @@ class NotificationsController {
if (!isset($grouped_events[$category])) {
$grouped_events[$category] = [];
}
-
+
// Add channels from settings
$event_id = $event['id'];
$event['channels'] = $settings[$event_id]['channels'] ?? [
'email' => ['enabled' => false, 'recipient' => $event['recipient_type']],
'push' => ['enabled' => false, 'recipient' => $event['recipient_type']]
];
-
+
$grouped_events[$category][] = $event;
}
-
+
return $grouped_events;
}
-
+
/**
* Update event settings
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error
*/
- public function update_event(WP_REST_Request $request) {
+ public function update_event(WP_REST_Request $request)
+ {
$params = $request->get_json_params();
$event_id = isset($params['eventId']) ? $params['eventId'] : null;
$channel_id = isset($params['channelId']) ? $params['channelId'] : null;
$enabled = isset($params['enabled']) ? $params['enabled'] : null;
$recipient = isset($params['recipient']) ? $params['recipient'] : null;
-
+
if (empty($event_id) || empty($channel_id)) {
return new WP_Error(
'invalid_params',
@@ -417,69 +426,72 @@ class NotificationsController {
['status' => 400]
);
}
-
+
// Get current settings
$settings = get_option('woonoow_notification_settings', []);
-
+
// Update settings
if (!isset($settings[$event_id])) {
$settings[$event_id] = ['channels' => []];
}
-
+
if (!isset($settings[$event_id]['channels'])) {
$settings[$event_id]['channels'] = [];
}
-
+
$settings[$event_id]['channels'][$channel_id] = [
'enabled' => (bool) $enabled,
'recipient' => $recipient ?? 'admin',
];
-
+
// Save settings
update_option('woonoow_notification_settings', $settings, false);
-
+
// Fire action for addons to react
do_action('woonoow_notification_event_updated', $event_id, $channel_id, $enabled, $recipient);
-
+
return new WP_REST_Response([
'success' => true,
'message' => __('Event settings updated successfully', 'woonoow'),
], 200);
}
-
+
/**
* Check if user has permission
*
* @return bool
*/
- public function check_permission() {
+ public function check_permission()
+ {
return current_user_can('manage_woocommerce') || current_user_can('manage_options');
}
-
+
/**
* Get all templates
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response
*/
- public function get_templates(WP_REST_Request $request) {
+ public function get_templates(WP_REST_Request $request)
+ {
$templates = TemplateProvider::get_templates();
return new WP_REST_Response($templates, 200);
}
-
+
/**
* Get single template
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error
*/
- public function get_template(WP_REST_Request $request) {
+ public function get_template(WP_REST_Request $request)
+ {
$event_id = $request->get_param('eventId');
$channel_id = $request->get_param('channelId');
$recipient_type = $request->get_param('recipient') ?? 'customer';
-
+
$template = TemplateProvider::get_template($event_id, $channel_id, $recipient_type);
-
+
if (!$template) {
return new WP_Error(
'template_not_found',
@@ -487,16 +499,16 @@ class NotificationsController {
['status' => 404]
);
}
-
+
// Add event and channel labels for UI
// Get events from controller method
$events_response = $this->get_events(new WP_REST_Request());
$events_data = $events_response->get_data();
-
+
// Get channels from controller method
$channels_response = $this->get_channels(new WP_REST_Request());
$channels_data = $channels_response->get_data();
-
+
// Find event label
foreach ($events_data as $category => $event_list) {
foreach ($event_list as $event) {
@@ -506,7 +518,7 @@ class NotificationsController {
}
}
}
-
+
// Find channel label
foreach ($channels_data as $channel) {
if ($channel['id'] === $channel_id) {
@@ -514,35 +526,36 @@ class NotificationsController {
break;
}
}
-
+
// Add available variables for this event (contextual)
$template['available_variables'] = EventRegistry::get_variables_for_event($event_id, $recipient_type);
-
+
return new WP_REST_Response($template, 200);
}
-
+
/**
* Save template
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error
*/
- public function save_template(WP_REST_Request $request) {
+ public function save_template(WP_REST_Request $request)
+ {
$event_id = $request->get_param('eventId');
$channel_id = $request->get_param('channelId');
$recipient_type = $request->get_param('recipient') ?? 'customer';
$subject = $request->get_param('subject');
$body = $request->get_param('body');
$variables = $request->get_param('variables');
-
+
$template = [
'subject' => $subject,
'body' => $body,
'variables' => $variables,
];
-
+
$result = TemplateProvider::save_template($event_id, $channel_id, $template, $recipient_type);
-
+
if (!$result) {
return new WP_Error(
'save_failed',
@@ -550,56 +563,59 @@ class NotificationsController {
['status' => 500]
);
}
-
+
return new WP_REST_Response([
'success' => true,
'message' => __('Template saved successfully', 'woonoow'),
], 200);
}
-
+
/**
* Delete template (revert to default)
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response
*/
- public function delete_template(WP_REST_Request $request) {
+ public function delete_template(WP_REST_Request $request)
+ {
$event_id = $request->get_param('eventId');
$channel_id = $request->get_param('channelId');
$recipient_type = $request->get_param('recipient') ?? 'customer';
-
+
TemplateProvider::delete_template($event_id, $channel_id, $recipient_type);
-
+
return new WP_REST_Response([
'success' => true,
'message' => __('Template reverted to default', 'woonoow'),
], 200);
}
-
+
/**
* Get VAPID public key for push notifications
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response
*/
- public function get_vapid_key(WP_REST_Request $request) {
+ public function get_vapid_key(WP_REST_Request $request)
+ {
$public_key = PushNotificationHandler::get_public_key();
-
+
return new WP_REST_Response([
'publicKey' => $public_key,
], 200);
}
-
+
/**
* Subscribe to push notifications
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error
*/
- public function push_subscribe(WP_REST_Request $request) {
+ public function push_subscribe(WP_REST_Request $request)
+ {
$subscription = $request->get_param('subscription');
$user_id = get_current_user_id();
-
+
if (empty($subscription)) {
return new WP_Error(
'invalid_subscription',
@@ -607,9 +623,9 @@ class NotificationsController {
['status' => 400]
);
}
-
+
$success = PushNotificationHandler::subscribe($subscription, $user_id);
-
+
if (!$success) {
return new WP_Error(
'subscribe_failed',
@@ -617,51 +633,54 @@ class NotificationsController {
['status' => 500]
);
}
-
+
return new WP_REST_Response([
'success' => true,
'message' => __('Subscribed to push notifications', 'woonoow'),
], 200);
}
-
+
/**
* Unsubscribe from push notifications
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response
*/
- public function push_unsubscribe(WP_REST_Request $request) {
+ public function push_unsubscribe(WP_REST_Request $request)
+ {
$subscription_id = $request->get_param('subscriptionId');
-
+
PushNotificationHandler::unsubscribe($subscription_id);
-
+
return new WP_REST_Response([
'success' => true,
'message' => __('Unsubscribed from push notifications', 'woonoow'),
], 200);
}
-
+
/**
* Get push notification settings
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response
*/
- public function get_push_settings(WP_REST_Request $request) {
+ public function get_push_settings(WP_REST_Request $request)
+ {
$settings = PushNotificationHandler::get_settings();
-
+
return new WP_REST_Response($settings, 200);
}
-
+
/**
* Update push notification settings
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response
*/
- public function update_push_settings(WP_REST_Request $request) {
+ public function update_push_settings(WP_REST_Request $request)
+ {
$settings = $request->get_json_params();
-
+
if (empty($settings)) {
return new WP_Error(
'invalid_settings',
@@ -669,9 +688,9 @@ class NotificationsController {
['status' => 400]
);
}
-
+
$success = PushNotificationHandler::update_settings($settings);
-
+
if (!$success) {
return new WP_Error(
'update_failed',
@@ -679,25 +698,26 @@ class NotificationsController {
['status' => 500]
);
}
-
+
return new WP_REST_Response([
'success' => true,
'message' => __('Push notification settings updated', 'woonoow'),
'settings' => PushNotificationHandler::get_settings(),
], 200);
}
-
+
/**
* Toggle notification channel
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response
*/
- public function toggle_channel(WP_REST_Request $request) {
+ public function toggle_channel(WP_REST_Request $request)
+ {
$params = $request->get_json_params();
$channel_id = isset($params['channelId']) ? $params['channelId'] : null;
$enabled = isset($params['enabled']) ? $params['enabled'] : null;
-
+
if (empty($channel_id)) {
return new WP_Error(
'invalid_channel',
@@ -705,7 +725,7 @@ class NotificationsController {
['status' => 400]
);
}
-
+
if ($enabled === null) {
return new WP_Error(
'invalid_enabled',
@@ -713,7 +733,7 @@ class NotificationsController {
['status' => 400]
);
}
-
+
// Only allow toggling built-in channels
$option_key = '';
if ($channel_id === 'email') {
@@ -727,13 +747,13 @@ class NotificationsController {
['status' => 400]
);
}
-
+
// Update the option
update_option($option_key, (bool) $enabled, false); // false = don't autoload
-
+
// Verify the update
$verified = get_option($option_key);
-
+
return new WP_REST_Response([
'success' => true,
'message' => sprintf(
@@ -745,11 +765,12 @@ class NotificationsController {
'enabled' => (bool) $verified,
], 200);
}
-
+
/**
* Get email customization settings
*/
- public function get_email_settings(WP_REST_Request $request) {
+ public function get_email_settings(WP_REST_Request $request)
+ {
$defaults = [
'primary_color' => '#7f54b3',
'secondary_color' => '#7f54b3',
@@ -764,26 +785,27 @@ class NotificationsController {
'footer_text' => '',
'social_links' => [],
];
-
+
$settings = get_option('woonoow_email_settings', $defaults);
-
+
// Ensure social_links is an array
if (!isset($settings['social_links']) || !is_array($settings['social_links'])) {
$settings['social_links'] = [];
}
-
+
// Merge with defaults to ensure all fields exist
$settings = array_merge($defaults, $settings);
-
+
return new WP_REST_Response($settings, 200);
}
-
+
/**
* Save email customization settings
*/
- public function save_email_settings(WP_REST_Request $request) {
+ public function save_email_settings(WP_REST_Request $request)
+ {
$data = $request->get_json_params();
-
+
$settings = [
'primary_color' => sanitize_hex_color($data['primary_color'] ?? '#7f54b3'),
'secondary_color' => sanitize_hex_color($data['secondary_color'] ?? '#7f54b3'),
@@ -798,47 +820,49 @@ class NotificationsController {
'footer_text' => sanitize_text_field($data['footer_text'] ?? ''),
'social_links' => $this->sanitize_social_links($data['social_links'] ?? []),
];
-
+
update_option('woonoow_email_settings', $settings);
-
+
return new WP_REST_Response([
'success' => true,
'message' => __('Email settings saved successfully', 'woonoow'),
'settings' => $settings,
], 200);
}
-
+
/**
* Reset email customization settings to defaults
*/
- public function reset_email_settings(WP_REST_Request $request) {
+ public function reset_email_settings(WP_REST_Request $request)
+ {
delete_option('woonoow_email_settings');
-
+
return new WP_REST_Response([
'success' => true,
'message' => __('Email settings reset to defaults', 'woonoow'),
], 200);
}
-
+
/**
* Sanitize social links array
*/
- private function sanitize_social_links($links) {
+ private function sanitize_social_links($links)
+ {
if (!is_array($links)) {
return [];
}
-
+
$sanitized = [];
$allowed_platforms = ['facebook', 'x', 'instagram', 'linkedin', 'youtube', 'discord', 'spotify', 'telegram', 'whatsapp', 'threads', 'website'];
-
+
foreach ($links as $link) {
if (!is_array($link) || !isset($link['platform']) || !isset($link['url'])) {
continue;
}
-
+
$platform = sanitize_text_field($link['platform']);
$url = esc_url_raw($link['url']);
-
+
if (in_array($platform, $allowed_platforms) && !empty($url)) {
$sanitized[] = [
'platform' => $platform,
@@ -846,19 +870,20 @@ class NotificationsController {
];
}
}
-
+
return $sanitized;
}
-
+
/**
* Get notification system mode
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response
*/
- public function get_system_mode(WP_REST_Request $request) {
+ public function get_system_mode(WP_REST_Request $request)
+ {
$mode = get_option('woonoow_notification_system_mode', 'woonoow');
-
+
return new WP_REST_Response([
'mode' => $mode,
'available_modes' => [
@@ -867,17 +892,18 @@ class NotificationsController {
],
], 200);
}
-
+
/**
* Set notification system mode
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error
*/
- public function set_system_mode(WP_REST_Request $request) {
+ public function set_system_mode(WP_REST_Request $request)
+ {
$params = $request->get_json_params();
$mode = isset($params['mode']) ? $params['mode'] : null;
-
+
if (!in_array($mode, ['woonoow', 'woocommerce'])) {
return new WP_Error(
'invalid_mode',
@@ -885,13 +911,13 @@ class NotificationsController {
['status' => 400]
);
}
-
+
// Update the mode
update_option('woonoow_notification_system_mode', $mode);
-
+
// Trigger action for other systems to react
do_action('woonoow_notification_system_mode_changed', $mode);
-
+
return new WP_REST_Response([
'success' => true,
'mode' => $mode,
@@ -901,49 +927,50 @@ class NotificationsController {
),
], 200);
}
-
+
/**
* Get notification activity logs
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response
*/
- public function get_logs(WP_REST_Request $request) {
+ public function get_logs(WP_REST_Request $request)
+ {
$page = (int) $request->get_param('page') ?: 1;
$per_page = (int) $request->get_param('per_page') ?: 20;
$channel = $request->get_param('channel');
$status = $request->get_param('status');
$search = $request->get_param('search');
-
+
// Get logs from option (in a real app, use a custom table)
$all_logs = get_option('woonoow_notification_logs', []);
-
+
// Apply filters
if ($channel && $channel !== 'all') {
$all_logs = array_filter($all_logs, fn($log) => $log['channel'] === $channel);
}
-
+
if ($status && $status !== 'all') {
$all_logs = array_filter($all_logs, fn($log) => $log['status'] === $status);
}
-
+
if ($search) {
$search_lower = strtolower($search);
- $all_logs = array_filter($all_logs, function($log) use ($search_lower) {
+ $all_logs = array_filter($all_logs, function ($log) use ($search_lower) {
return strpos(strtolower($log['recipient'] ?? ''), $search_lower) !== false ||
- strpos(strtolower($log['subject'] ?? ''), $search_lower) !== false;
+ strpos(strtolower($log['subject'] ?? ''), $search_lower) !== false;
});
}
-
+
// Sort by date descending
- usort($all_logs, function($a, $b) {
+ usort($all_logs, function ($a, $b) {
return strtotime($b['created_at'] ?? '') - strtotime($a['created_at'] ?? '');
});
-
+
$total = count($all_logs);
$offset = ($page - 1) * $per_page;
$logs = array_slice(array_values($all_logs), $offset, $per_page);
-
+
return new WP_REST_Response([
'logs' => $logs,
'total' => $total,
@@ -951,19 +978,20 @@ class NotificationsController {
'per_page' => $per_page,
], 200);
}
-
+
/**
* Send test email for a notification template
*
* @param WP_REST_Request $request Request object
* @return WP_REST_Response|WP_Error
*/
- public function send_test_email(WP_REST_Request $request) {
+ public function send_test_email(WP_REST_Request $request)
+ {
$event_id = $request->get_param('eventId');
$channel_id = $request->get_param('channelId');
$recipient_type = $request->get_param('recipient') ?? 'customer';
$to_email = $request->get_param('email');
-
+
// Validate email
if (!is_email($to_email)) {
return new \WP_Error(
@@ -972,7 +1000,7 @@ class NotificationsController {
['status' => 400]
);
}
-
+
// Only support email channel for test
if ($channel_id !== 'email') {
return new \WP_Error(
@@ -981,10 +1009,10 @@ class NotificationsController {
['status' => 400]
);
}
-
+
// Get template
$template = TemplateProvider::get_template($event_id, $channel_id, $recipient_type);
-
+
if (!$template) {
return new \WP_Error(
'template_not_found',
@@ -992,26 +1020,26 @@ class NotificationsController {
['status' => 404]
);
}
-
+
// Build sample data for variables
$sample_data = $this->get_sample_data_for_event($event_id);
-
+
// Replace variables in subject and body
$subject = '[TEST] ' . $this->replace_variables($template['subject'] ?? '', $sample_data);
$body_markdown = $this->replace_variables($template['body'] ?? '', $sample_data);
-
+
// Render email using EmailRenderer
$email_renderer = \WooNooW\Core\Notifications\EmailRenderer::instance();
-
+
// We need to manually render since we're not triggering a real event
$html = $this->render_test_email($body_markdown, $subject, $sample_data);
-
+
// Set content type to HTML
$headers = ['Content-Type: text/html; charset=UTF-8'];
-
+
// Send email
$sent = wp_mail($to_email, $subject, $html, $headers);
-
+
if (!$sent) {
return new \WP_Error(
'send_failed',
@@ -1019,24 +1047,25 @@ class NotificationsController {
['status' => 500]
);
}
-
+
return new WP_REST_Response([
'success' => true,
'message' => sprintf(__('Test email sent to %s', 'woonoow'), $to_email),
], 200);
}
-
+
/**
* Get sample data for an event type
*
* @param string $event_id
* @return array
*/
- private function get_sample_data_for_event($event_id) {
+ private function get_sample_data_for_event($event_id)
+ {
$base_data = [
'site_name' => get_bloginfo('name'),
'store_name' => get_bloginfo('name'),
- 'store_url' => home_url(),
+ 'site_url' => home_url(),
'shop_url' => get_permalink(wc_get_page_id('shop')),
'my_account_url' => get_permalink(wc_get_page_id('myaccount')),
'support_email' => get_option('admin_email'),
@@ -1048,7 +1077,7 @@ class NotificationsController {
'customer_phone' => '+1 234 567 8900',
'login_url' => wp_login_url(),
];
-
+
// Order-related events
if (strpos($event_id, 'order') !== false) {
$base_data = array_merge($base_data, [
@@ -1080,7 +1109,7 @@ class NotificationsController {
'order_items_table' => $this->get_sample_order_items_html(),
]);
}
-
+
// Customer account events
if (strpos($event_id, 'customer') !== false || strpos($event_id, 'account') !== false) {
$base_data = array_merge($base_data, [
@@ -1092,16 +1121,17 @@ class NotificationsController {
'user_email' => 'john@example.com',
]);
}
-
+
return $base_data;
}
-
+
/**
* Get sample order items HTML
*
* @return string
*/
- private function get_sample_order_items_html() {
+ private function get_sample_order_items_html()
+ {
return '
@@ -1130,7 +1160,7 @@ class NotificationsController {
';
}
-
+
/**
* Replace variables in text
*
@@ -1138,13 +1168,14 @@ class NotificationsController {
* @param array $variables
* @return string
*/
- private function replace_variables($text, $variables) {
+ private function replace_variables($text, $variables)
+ {
foreach ($variables as $key => $value) {
$text = str_replace('{' . $key . '}', $value, $text);
}
return $text;
}
-
+
/**
* Render test email HTML
*
@@ -1153,10 +1184,11 @@ class NotificationsController {
* @param array $variables
* @return string
*/
- private function render_test_email($body_markdown, $subject, $variables) {
+ private function render_test_email($body_markdown, $subject, $variables)
+ {
// Parse cards
$content = $this->parse_cards_for_test($body_markdown);
-
+
// Get appearance settings for colors
$appearance = get_option('woonoow_appearance_settings', []);
$colors = $appearance['general']['colors'] ?? [];
@@ -1164,30 +1196,30 @@ class NotificationsController {
$secondary_color = $colors['secondary'] ?? '#7f54b3';
$hero_gradient_start = $colors['gradientStart'] ?? '#667eea';
$hero_gradient_end = $colors['gradientEnd'] ?? '#764ba2';
-
+
// Get email settings for branding
$email_settings = get_option('woonoow_email_settings', []);
$logo_url = $email_settings['logo_url'] ?? '';
$header_text = $email_settings['header_text'] ?? $variables['store_name'];
$footer_text = $email_settings['footer_text'] ?? sprintf('© %s %s. All rights reserved.', date('Y'), $variables['store_name']);
$footer_text = str_replace('{current_year}', date('Y'), $footer_text);
-
+
// Build header
if (!empty($logo_url)) {
$header = sprintf(
'
',
- esc_url($variables['store_url']),
+ esc_url($variables['site_url']),
esc_url($logo_url),
esc_attr($variables['store_name'])
);
} else {
$header = sprintf(
'%s',
- esc_url($variables['store_url']),
+ esc_url($variables['site_url']),
esc_html($header_text)
);
}
-
+
// Build full HTML
$html = '
@@ -1222,32 +1254,33 @@ class NotificationsController {