fix: resolve container width issues, spa redirects, and appearance settings overwrite. feat: enhance order/sub details and newsletter layout
This commit is contained in:
@@ -1,41 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace WooNooW\Admin;
|
||||
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_Error;
|
||||
|
||||
class AppearanceController {
|
||||
|
||||
class AppearanceController
|
||||
{
|
||||
|
||||
const OPTION_KEY = 'woonoow_appearance_settings';
|
||||
const API_NAMESPACE = 'woonoow/v1';
|
||||
|
||||
public static function init() {
|
||||
|
||||
public static function init()
|
||||
{
|
||||
add_action('rest_api_init', [__CLASS__, 'register_routes']);
|
||||
}
|
||||
|
||||
public static function register_routes() {
|
||||
|
||||
public static function register_routes()
|
||||
{
|
||||
// Get all settings (public access for frontend)
|
||||
register_rest_route(self::API_NAMESPACE, '/appearance/settings', [
|
||||
'methods' => '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<page>[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,
|
||||
],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Campaigns REST Controller
|
||||
*
|
||||
@@ -13,64 +14,67 @@ use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_Error;
|
||||
use WooNooW\Core\Campaigns\CampaignManager;
|
||||
use WooNooW\Core\ActivityLog\Logger;
|
||||
|
||||
class CampaignsController
|
||||
{
|
||||
|
||||
class CampaignsController {
|
||||
|
||||
const API_NAMESPACE = 'woonoow/v1';
|
||||
|
||||
|
||||
/**
|
||||
* Register REST routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
public static function register_routes()
|
||||
{
|
||||
// List campaigns
|
||||
register_rest_route(self::API_NAMESPACE, '/campaigns', [
|
||||
'methods' => '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<id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [__CLASS__, 'get_campaign'],
|
||||
'permission_callback' => [__CLASS__, 'check_admin_permission'],
|
||||
]);
|
||||
|
||||
|
||||
// Update campaign
|
||||
register_rest_route(self::API_NAMESPACE, '/campaigns/(?P<id>\d+)', [
|
||||
'methods' => 'PUT',
|
||||
'callback' => [__CLASS__, 'update_campaign'],
|
||||
'permission_callback' => [__CLASS__, 'check_admin_permission'],
|
||||
]);
|
||||
|
||||
|
||||
// Delete campaign
|
||||
register_rest_route(self::API_NAMESPACE, '/campaigns/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [__CLASS__, 'delete_campaign'],
|
||||
'permission_callback' => [__CLASS__, 'check_admin_permission'],
|
||||
]);
|
||||
|
||||
|
||||
// Send campaign
|
||||
register_rest_route(self::API_NAMESPACE, '/campaigns/(?P<id>\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<id>\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<id>\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,
|
||||
|
||||
@@ -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)
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace WooNooW\API;
|
||||
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_Error;
|
||||
use WooNooW\Core\Validation;
|
||||
use WooNooW\Database\SubscriberTable;
|
||||
|
||||
class NewsletterController {
|
||||
class NewsletterController
|
||||
{
|
||||
const API_NAMESPACE = 'woonoow/v1';
|
||||
|
||||
public static function register_routes() {
|
||||
|
||||
public static function register_routes()
|
||||
{
|
||||
register_rest_route(self::API_NAMESPACE, '/newsletter/subscribe', [
|
||||
'methods' => '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<email>[^/]+)', [
|
||||
'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<template>[^/]+)', [
|
||||
'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<template>[^/]+)', [
|
||||
'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(
|
||||
'<!DOCTYPE html><html><head><title>%s</title><style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#f5f5f5;}.box{background:white;padding:40px;border-radius:8px;text-align:center;box-shadow:0 2px 10px rgba(0,0,0,0.1);max-width:400px;}h1{color:#333;margin-bottom:16px;}p{color:#666;}a{display:inline-block;margin-top:20px;padding:12px 24px;background:#333;color:white;text-decoration:none;border-radius:6px;}</style></head><body><div class="box"><h1>✓ Confirmed!</h1><p>You are now subscribed to %s newsletter.</p><a href="%s">Continue Shopping</a></div></body></html>',
|
||||
__('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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -371,13 +371,13 @@ class ProductsController
|
||||
}
|
||||
|
||||
// Virtual and downloadable
|
||||
if (isset($data['virtual'])) {
|
||||
if (array_key_exists('virtual', $data)) {
|
||||
$product->set_virtual((bool) $data['virtual']);
|
||||
}
|
||||
if (isset($data['downloadable'])) {
|
||||
if (array_key_exists('downloadable', $data)) {
|
||||
$product->set_downloadable((bool) $data['downloadable']);
|
||||
}
|
||||
if (isset($data['featured'])) {
|
||||
if (array_key_exists('featured', $data)) {
|
||||
$product->set_featured((bool) $data['featured']);
|
||||
}
|
||||
|
||||
@@ -510,13 +510,13 @@ class ProductsController
|
||||
if (isset($data['height'])) $product->set_height(self::sanitize_number($data['height']));
|
||||
|
||||
// Virtual and downloadable
|
||||
if (isset($data['virtual'])) {
|
||||
if (array_key_exists('virtual', $data)) {
|
||||
$product->set_virtual((bool) $data['virtual']);
|
||||
}
|
||||
if (isset($data['downloadable'])) {
|
||||
if (array_key_exists('downloadable', $data)) {
|
||||
$product->set_downloadable((bool) $data['downloadable']);
|
||||
}
|
||||
if (isset($data['featured'])) {
|
||||
if (array_key_exists('featured', $data)) {
|
||||
$product->set_featured((bool) $data['featured']);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Store REST API Controller
|
||||
*
|
||||
@@ -11,28 +12,31 @@ namespace WooNooW\API;
|
||||
|
||||
use WooNooW\Compat\StoreSettingsProvider;
|
||||
use WooNooW\Compat\CustomerSettingsProvider;
|
||||
use WooNooW\Compat\SecuritySettingsProvider;
|
||||
use WP_REST_Controller;
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_Error;
|
||||
|
||||
class StoreController extends WP_REST_Controller {
|
||||
|
||||
class StoreController extends WP_REST_Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Namespace
|
||||
*/
|
||||
protected $namespace = 'woonoow/v1';
|
||||
|
||||
|
||||
/**
|
||||
* Rest base
|
||||
*/
|
||||
protected $rest_base = 'store';
|
||||
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public function register_routes() {
|
||||
public function register_routes()
|
||||
{
|
||||
// GET /woonoow/v1/store/branding (PUBLIC - for login page)
|
||||
register_rest_route($this->namespace, '/' . $this->rest_base . '/branding', [
|
||||
[
|
||||
@@ -41,7 +45,7 @@ class StoreController extends WP_REST_Controller {
|
||||
'permission_callback' => '__return_true', // Public endpoint
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
// GET /woonoow/v1/store/settings
|
||||
register_rest_route($this->namespace, '/' . $this->rest_base . '/settings', [
|
||||
[
|
||||
@@ -50,7 +54,7 @@ class StoreController extends WP_REST_Controller {
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
// POST /woonoow/v1/store/settings
|
||||
register_rest_route($this->namespace, '/' . $this->rest_base . '/settings', [
|
||||
[
|
||||
@@ -59,7 +63,7 @@ class StoreController extends WP_REST_Controller {
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
// GET /woonoow/v1/store/countries
|
||||
register_rest_route($this->namespace, '/' . $this->rest_base . '/countries', [
|
||||
[
|
||||
@@ -68,7 +72,7 @@ class StoreController extends WP_REST_Controller {
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
// GET /woonoow/v1/store/timezones
|
||||
register_rest_route($this->namespace, '/' . $this->rest_base . '/timezones', [
|
||||
[
|
||||
@@ -77,7 +81,7 @@ class StoreController extends WP_REST_Controller {
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
// GET /woonoow/v1/store/currencies
|
||||
register_rest_route($this->namespace, '/' . $this->rest_base . '/currencies', [
|
||||
[
|
||||
@@ -86,7 +90,7 @@ class StoreController extends WP_REST_Controller {
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
// GET /woonoow/v1/store/customer-settings
|
||||
register_rest_route($this->namespace, '/' . $this->rest_base . '/customer-settings', [
|
||||
[
|
||||
@@ -95,7 +99,7 @@ class StoreController extends WP_REST_Controller {
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
// POST /woonoow/v1/store/customer-settings
|
||||
register_rest_route($this->namespace, '/' . $this->rest_base . '/customer-settings', [
|
||||
[
|
||||
@@ -104,15 +108,34 @@ class StoreController extends WP_REST_Controller {
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
],
|
||||
]);
|
||||
|
||||
// GET /woonoow/v1/store/security-settings
|
||||
register_rest_route($this->namespace, '/' . $this->rest_base . '/security-settings', [
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'get_security_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
],
|
||||
]);
|
||||
|
||||
// POST /woonoow/v1/store/security-settings
|
||||
register_rest_route($this->namespace, '/' . $this->rest_base . '/security-settings', [
|
||||
[
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => [$this, 'save_security_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get store branding (PUBLIC - for login page)
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response Response object
|
||||
*/
|
||||
public function get_branding(WP_REST_Request $request) {
|
||||
public function get_branding(WP_REST_Request $request)
|
||||
{
|
||||
$branding = [
|
||||
'store_name' => get_option('woonoow_store_name', '') ?: get_option('blogname', 'WooNooW'),
|
||||
'store_logo' => get_option('woonoow_store_logo', ''),
|
||||
@@ -120,26 +143,27 @@ class StoreController extends WP_REST_Controller {
|
||||
'store_icon' => get_option('woonoow_store_icon', ''),
|
||||
'store_tagline' => get_option('woonoow_store_tagline', ''),
|
||||
];
|
||||
|
||||
|
||||
$response = rest_ensure_response($branding);
|
||||
$response->header('Cache-Control', 'max-age=300'); // Cache for 5 minutes
|
||||
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get store settings
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error Response object or error
|
||||
*/
|
||||
public function get_settings(WP_REST_Request $request) {
|
||||
public function get_settings(WP_REST_Request $request)
|
||||
{
|
||||
try {
|
||||
$settings = StoreSettingsProvider::get_settings();
|
||||
|
||||
|
||||
$response = rest_ensure_response($settings);
|
||||
$response->header('Cache-Control', 'max-age=60');
|
||||
|
||||
|
||||
return $response;
|
||||
} catch (\Exception $e) {
|
||||
return new WP_Error(
|
||||
@@ -149,16 +173,17 @@ class StoreController extends WP_REST_Controller {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save store settings
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error Response object or error
|
||||
*/
|
||||
public function save_settings(WP_REST_Request $request) {
|
||||
public function save_settings(WP_REST_Request $request)
|
||||
{
|
||||
$settings = $request->get_json_params();
|
||||
|
||||
|
||||
if (empty($settings)) {
|
||||
return new WP_Error(
|
||||
'missing_settings',
|
||||
@@ -166,10 +191,10 @@ class StoreController extends WP_REST_Controller {
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
$result = StoreSettingsProvider::save_settings($settings);
|
||||
|
||||
|
||||
if (!$result) {
|
||||
return new WP_Error(
|
||||
'save_failed',
|
||||
@@ -177,7 +202,7 @@ class StoreController extends WP_REST_Controller {
|
||||
['status' => 500]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return rest_ensure_response([
|
||||
'success' => true,
|
||||
'message' => 'Settings saved successfully',
|
||||
@@ -191,20 +216,21 @@ class StoreController extends WP_REST_Controller {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get countries
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error Response object or error
|
||||
*/
|
||||
public function get_countries(WP_REST_Request $request) {
|
||||
public function get_countries(WP_REST_Request $request)
|
||||
{
|
||||
try {
|
||||
$countries = StoreSettingsProvider::get_countries();
|
||||
|
||||
|
||||
$response = rest_ensure_response($countries);
|
||||
$response->header('Cache-Control', 'max-age=3600'); // Cache for 1 hour
|
||||
|
||||
|
||||
return $response;
|
||||
} catch (\Exception $e) {
|
||||
return new WP_Error(
|
||||
@@ -214,20 +240,21 @@ class StoreController extends WP_REST_Controller {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get timezones
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error Response object or error
|
||||
*/
|
||||
public function get_timezones(WP_REST_Request $request) {
|
||||
public function get_timezones(WP_REST_Request $request)
|
||||
{
|
||||
try {
|
||||
$timezones = StoreSettingsProvider::get_timezones();
|
||||
|
||||
|
||||
$response = rest_ensure_response($timezones);
|
||||
$response->header('Cache-Control', 'max-age=3600'); // Cache for 1 hour
|
||||
|
||||
|
||||
return $response;
|
||||
} catch (\Exception $e) {
|
||||
return new WP_Error(
|
||||
@@ -237,20 +264,21 @@ class StoreController extends WP_REST_Controller {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get currencies
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error Response object or error
|
||||
*/
|
||||
public function get_currencies(WP_REST_Request $request) {
|
||||
public function get_currencies(WP_REST_Request $request)
|
||||
{
|
||||
try {
|
||||
$currencies = StoreSettingsProvider::get_currencies();
|
||||
|
||||
|
||||
$response = rest_ensure_response($currencies);
|
||||
$response->header('Cache-Control', 'max-age=3600'); // Cache for 1 hour
|
||||
|
||||
|
||||
return $response;
|
||||
} catch (\Exception $e) {
|
||||
return new WP_Error(
|
||||
@@ -260,20 +288,21 @@ class StoreController extends WP_REST_Controller {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get customer settings
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error Response object or error
|
||||
*/
|
||||
public function get_customer_settings(WP_REST_Request $request) {
|
||||
public function get_customer_settings(WP_REST_Request $request)
|
||||
{
|
||||
try {
|
||||
$settings = CustomerSettingsProvider::get_settings();
|
||||
|
||||
|
||||
$response = rest_ensure_response($settings);
|
||||
$response->header('Cache-Control', 'max-age=60');
|
||||
|
||||
|
||||
return $response;
|
||||
} catch (\Exception $e) {
|
||||
return new WP_Error(
|
||||
@@ -283,17 +312,18 @@ class StoreController extends WP_REST_Controller {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save customer settings
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error Response object or error
|
||||
*/
|
||||
public function save_customer_settings(WP_REST_Request $request) {
|
||||
public function save_customer_settings(WP_REST_Request $request)
|
||||
{
|
||||
try {
|
||||
$settings = $request->get_json_params();
|
||||
|
||||
|
||||
if (empty($settings)) {
|
||||
return new WP_Error(
|
||||
'invalid_settings',
|
||||
@@ -301,9 +331,9 @@ class StoreController extends WP_REST_Controller {
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
$updated = CustomerSettingsProvider::update_settings($settings);
|
||||
|
||||
|
||||
if (!$updated) {
|
||||
return new WP_Error(
|
||||
'update_failed',
|
||||
@@ -311,16 +341,15 @@ class StoreController extends WP_REST_Controller {
|
||||
['status' => 500]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Return updated settings
|
||||
$new_settings = CustomerSettingsProvider::get_settings();
|
||||
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'message' => __('Customer settings updated successfully', 'woonoow'),
|
||||
'settings' => $new_settings,
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return new WP_Error(
|
||||
'save_customer_settings_failed',
|
||||
@@ -329,13 +358,84 @@ class StoreController extends WP_REST_Controller {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get security settings
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error Response object or error
|
||||
*/
|
||||
public function get_security_settings(WP_REST_Request $request)
|
||||
{
|
||||
try {
|
||||
$settings = SecuritySettingsProvider::get_settings();
|
||||
|
||||
$response = rest_ensure_response($settings);
|
||||
$response->header('Cache-Control', 'max-age=60');
|
||||
|
||||
return $response;
|
||||
} catch (\Exception $e) {
|
||||
return new WP_Error(
|
||||
'get_security_settings_failed',
|
||||
$e->getMessage(),
|
||||
['status' => 500]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save security settings
|
||||
*
|
||||
* @param WP_REST_Request $request Request object
|
||||
* @return WP_REST_Response|WP_Error Response object or error
|
||||
*/
|
||||
public function save_security_settings(WP_REST_Request $request)
|
||||
{
|
||||
try {
|
||||
$settings = $request->get_json_params();
|
||||
|
||||
if (empty($settings)) {
|
||||
return new WP_Error(
|
||||
'invalid_settings',
|
||||
__('Invalid settings data', 'woonoow'),
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
$updated = SecuritySettingsProvider::update_settings($settings);
|
||||
|
||||
if (!$updated) {
|
||||
return new WP_Error(
|
||||
'update_failed',
|
||||
__('Failed to update security settings', 'woonoow'),
|
||||
['status' => 500]
|
||||
);
|
||||
}
|
||||
|
||||
// Return updated settings
|
||||
$new_settings = SecuritySettingsProvider::get_settings();
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'message' => __('Security settings updated successfully', 'woonoow'),
|
||||
'settings' => $new_settings,
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
return new WP_Error(
|
||||
'save_security_settings_failed',
|
||||
$e->getMessage(),
|
||||
['status' => 500]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has permission
|
||||
*
|
||||
* @return bool True if user has permission
|
||||
*/
|
||||
public function check_permission() {
|
||||
public function check_permission()
|
||||
{
|
||||
// Check WooCommerce capability first, fallback to manage_options
|
||||
return current_user_can('manage_woocommerce') || current_user_can('manage_options');
|
||||
}
|
||||
|
||||
@@ -471,6 +471,31 @@ class SubscriptionsController
|
||||
}
|
||||
$enriched['billing_schedule'] = sprintf(__('Every %s%s', 'woonoow'), $interval, $period);
|
||||
|
||||
// Add payment method title
|
||||
$payment_title = $subscription->payment_method; // Default to ID
|
||||
|
||||
// 1. Try from payment_meta (stored snapshot)
|
||||
if (!empty($subscription->payment_meta)) {
|
||||
$meta = json_decode($subscription->payment_meta, true);
|
||||
if (isset($meta['method_title']) && !empty($meta['method_title'])) {
|
||||
$payment_title = $meta['method_title'];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If it looks like an ID (no spaces, lowercase), try to get fresh title from gateway
|
||||
if ($payment_title === $subscription->payment_method && function_exists('WC')) {
|
||||
$gateways_handler = WC()->payment_gateways();
|
||||
if ($gateways_handler) {
|
||||
$gateways = $gateways_handler->payment_gateways();
|
||||
if (isset($gateways[$subscription->payment_method])) {
|
||||
$gw = $gateways[$subscription->payment_method];
|
||||
$payment_title = $gw->get_title() ?: $gw->method_title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$enriched['payment_method_title'] = $payment_title;
|
||||
|
||||
return $enriched;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace WooNooW\Compat;
|
||||
|
||||
if ( ! defined('ABSPATH') ) exit;
|
||||
if (! defined('ABSPATH')) exit;
|
||||
|
||||
/**
|
||||
* Navigation Registry
|
||||
@@ -11,36 +12,39 @@ if ( ! defined('ABSPATH') ) exit;
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class NavigationRegistry {
|
||||
class NavigationRegistry
|
||||
{
|
||||
const NAV_OPTION = 'wnw_nav_tree';
|
||||
const NAV_VERSION = '1.3.0'; // Added Subscriptions section
|
||||
|
||||
|
||||
/**
|
||||
* Initialize hooks
|
||||
*/
|
||||
public static function init() {
|
||||
public static function init()
|
||||
{
|
||||
// Use 'init' hook instead of 'plugins_loaded' to avoid translation loading warnings (WP 6.7+)
|
||||
add_action('init', [__CLASS__, 'build_nav_tree'], 10);
|
||||
add_action('activated_plugin', [__CLASS__, 'flush']);
|
||||
add_action('deactivated_plugin', [__CLASS__, 'flush']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build the complete navigation tree
|
||||
*/
|
||||
public static function build_nav_tree() {
|
||||
public static function build_nav_tree()
|
||||
{
|
||||
// Check if we need to rebuild (version mismatch)
|
||||
$cached = get_option(self::NAV_OPTION, []);
|
||||
$cached_version = $cached['version'] ?? '';
|
||||
|
||||
|
||||
if ($cached_version === self::NAV_VERSION && !empty($cached['tree'])) {
|
||||
// Cache is valid, no need to rebuild
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Base navigation tree (core WooNooW sections)
|
||||
$tree = self::get_base_tree();
|
||||
|
||||
|
||||
/**
|
||||
* Filter: woonoow/nav_tree
|
||||
*
|
||||
@@ -64,7 +68,7 @@ class NavigationRegistry {
|
||||
* });
|
||||
*/
|
||||
$tree = apply_filters('woonoow/nav_tree', $tree);
|
||||
|
||||
|
||||
// Allow per-section modification
|
||||
foreach ($tree as &$section) {
|
||||
$key = $section['key'] ?? '';
|
||||
@@ -90,7 +94,7 @@ class NavigationRegistry {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Store in option
|
||||
update_option(self::NAV_OPTION, [
|
||||
'version' => self::NAV_VERSION,
|
||||
@@ -98,13 +102,14 @@ class NavigationRegistry {
|
||||
'updated' => time(),
|
||||
], false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get base navigation tree (core sections)
|
||||
*
|
||||
* @return array Base navigation tree
|
||||
*/
|
||||
private static function get_base_tree(): array {
|
||||
private static function get_base_tree(): array
|
||||
{
|
||||
$tree = [
|
||||
[
|
||||
'key' => 'dashboard',
|
||||
@@ -198,37 +203,39 @@ class NavigationRegistry {
|
||||
'children' => [], // Empty array = no submenu bar
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
return $tree;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get marketing submenu children
|
||||
*
|
||||
* @return array Marketing submenu items
|
||||
*/
|
||||
private static function get_marketing_children(): array {
|
||||
private static function get_marketing_children(): array
|
||||
{
|
||||
$children = [];
|
||||
|
||||
|
||||
// Newsletter - only if module enabled
|
||||
if (\WooNooW\Core\ModuleRegistry::is_enabled('newsletter')) {
|
||||
$children[] = ['label' => __('Newsletter', 'woonoow'), 'mode' => 'spa', 'path' => '/marketing/newsletter'];
|
||||
}
|
||||
|
||||
|
||||
// Coupons - always available
|
||||
$children[] = ['label' => __('Coupons', 'woonoow'), 'mode' => 'spa', 'path' => '/coupons'];
|
||||
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get settings submenu children
|
||||
*
|
||||
* @return array Settings submenu items
|
||||
*/
|
||||
private static function get_settings_children(): array {
|
||||
private static function get_settings_children(): array
|
||||
{
|
||||
$admin = admin_url('admin.php');
|
||||
|
||||
|
||||
$children = [
|
||||
// Core Settings (Shopify-inspired)
|
||||
['label' => __('Store Details', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/store'],
|
||||
@@ -236,25 +243,27 @@ class NavigationRegistry {
|
||||
['label' => __('Shipping & Delivery', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/shipping'],
|
||||
['label' => __('Tax', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/tax'],
|
||||
['label' => __('Customers', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/customers'],
|
||||
['label' => __('Security', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/security'],
|
||||
['label' => __('Notifications', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/notifications'],
|
||||
['label' => __('Modules', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/modules'],
|
||||
['label' => __('Developer', 'woonoow'), 'mode' => 'spa', 'path' => '/settings/developer'],
|
||||
];
|
||||
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get subscriptions navigation section
|
||||
* Returns empty array if module is not enabled
|
||||
*
|
||||
* @return array Subscriptions section or empty array
|
||||
*/
|
||||
private static function get_subscriptions_section(): array {
|
||||
private static function get_subscriptions_section(): array
|
||||
{
|
||||
if (!\WooNooW\Core\ModuleRegistry::is_enabled('subscription')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
return [
|
||||
[
|
||||
'key' => 'subscriptions',
|
||||
@@ -267,24 +276,26 @@ class NavigationRegistry {
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the complete navigation tree
|
||||
*
|
||||
* @return array Navigation tree
|
||||
*/
|
||||
public static function get_nav_tree(): array {
|
||||
public static function get_nav_tree(): array
|
||||
{
|
||||
$data = get_option(self::NAV_OPTION, []);
|
||||
return $data['tree'] ?? self::get_base_tree();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a specific section by key
|
||||
*
|
||||
* @param string $key Section key
|
||||
* @return array|null Section data or null if not found
|
||||
*/
|
||||
public static function get_section(string $key): ?array {
|
||||
public static function get_section(string $key): ?array
|
||||
{
|
||||
$tree = self::get_nav_tree();
|
||||
foreach ($tree as $section) {
|
||||
if (($section['key'] ?? '') === $key) {
|
||||
@@ -293,22 +304,24 @@ class NavigationRegistry {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Flush navigation cache
|
||||
*/
|
||||
public static function flush() {
|
||||
public static function flush()
|
||||
{
|
||||
delete_option(self::NAV_OPTION);
|
||||
// Rebuild immediately after flush
|
||||
self::build_nav_tree();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get navigation tree for frontend
|
||||
*
|
||||
* @return array Array suitable for JSON encoding
|
||||
*/
|
||||
public static function get_frontend_nav_tree(): array {
|
||||
public static function get_frontend_nav_tree(): array
|
||||
{
|
||||
return self::get_nav_tree();
|
||||
}
|
||||
}
|
||||
|
||||
301
includes/Compat/SecuritySettingsProvider.php
Normal file
301
includes/Compat/SecuritySettingsProvider.php
Normal file
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Security Settings Provider
|
||||
*
|
||||
* Provides security-related settings including rate limiting and CAPTCHA.
|
||||
*
|
||||
* @package WooNooW
|
||||
*/
|
||||
|
||||
namespace WooNooW\Compat;
|
||||
|
||||
class SecuritySettingsProvider
|
||||
{
|
||||
|
||||
/**
|
||||
* Get security settings
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_settings()
|
||||
{
|
||||
return [
|
||||
// Rate Limiting
|
||||
'enable_checkout_rate_limit' => get_option('woonoow_enable_checkout_rate_limit', 'yes') === 'yes',
|
||||
'rate_limit_orders' => intval(get_option('woonoow_rate_limit_orders', 5)),
|
||||
'rate_limit_minutes' => intval(get_option('woonoow_rate_limit_minutes', 10)),
|
||||
|
||||
// CAPTCHA
|
||||
'captcha_provider' => get_option('woonoow_captcha_provider', 'none'), // none, recaptcha, turnstile
|
||||
'recaptcha_site_key' => get_option('woonoow_recaptcha_site_key', ''),
|
||||
'recaptcha_secret_key' => get_option('woonoow_recaptcha_secret_key', ''),
|
||||
'turnstile_site_key' => get_option('woonoow_turnstile_site_key', ''),
|
||||
'turnstile_secret_key' => get_option('woonoow_turnstile_secret_key', ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public settings (safe to expose to frontend)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_public_settings()
|
||||
{
|
||||
$settings = self::get_settings();
|
||||
|
||||
return [
|
||||
'captcha_provider' => $settings['captcha_provider'],
|
||||
'recaptcha_site_key' => $settings['recaptcha_site_key'],
|
||||
'turnstile_site_key' => $settings['turnstile_site_key'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update security settings
|
||||
*
|
||||
* @param array $settings
|
||||
* @return bool
|
||||
*/
|
||||
public static function update_settings($settings)
|
||||
{
|
||||
// Rate Limiting
|
||||
if (array_key_exists('enable_checkout_rate_limit', $settings)) {
|
||||
$value = !empty($settings['enable_checkout_rate_limit']) ? 'yes' : 'no';
|
||||
update_option('woonoow_enable_checkout_rate_limit', $value);
|
||||
}
|
||||
|
||||
if (isset($settings['rate_limit_orders'])) {
|
||||
$value = max(1, intval($settings['rate_limit_orders']));
|
||||
update_option('woonoow_rate_limit_orders', $value);
|
||||
}
|
||||
|
||||
if (isset($settings['rate_limit_minutes'])) {
|
||||
$value = max(1, intval($settings['rate_limit_minutes']));
|
||||
update_option('woonoow_rate_limit_minutes', $value);
|
||||
}
|
||||
|
||||
// CAPTCHA Provider
|
||||
if (isset($settings['captcha_provider'])) {
|
||||
$valid_providers = ['none', 'recaptcha', 'turnstile'];
|
||||
$value = in_array($settings['captcha_provider'], $valid_providers)
|
||||
? $settings['captcha_provider']
|
||||
: 'none';
|
||||
update_option('woonoow_captcha_provider', $value);
|
||||
}
|
||||
|
||||
// reCAPTCHA Keys
|
||||
if (isset($settings['recaptcha_site_key'])) {
|
||||
update_option('woonoow_recaptcha_site_key', sanitize_text_field($settings['recaptcha_site_key']));
|
||||
}
|
||||
|
||||
if (isset($settings['recaptcha_secret_key'])) {
|
||||
update_option('woonoow_recaptcha_secret_key', sanitize_text_field($settings['recaptcha_secret_key']));
|
||||
}
|
||||
|
||||
// Turnstile Keys
|
||||
if (isset($settings['turnstile_site_key'])) {
|
||||
update_option('woonoow_turnstile_site_key', sanitize_text_field($settings['turnstile_site_key']));
|
||||
}
|
||||
|
||||
if (isset($settings['turnstile_secret_key'])) {
|
||||
update_option('woonoow_turnstile_secret_key', sanitize_text_field($settings['turnstile_secret_key']));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if rate limit is exceeded for an IP
|
||||
*
|
||||
* @param string|null $ip IP address (null = auto-detect)
|
||||
* @return bool True if rate limit exceeded
|
||||
*/
|
||||
public static function is_rate_limited($ip = null)
|
||||
{
|
||||
$settings = self::get_settings();
|
||||
|
||||
if (!$settings['enable_checkout_rate_limit']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($ip === null) {
|
||||
$ip = self::get_client_ip();
|
||||
}
|
||||
|
||||
$transient_key = 'woonoow_rate_' . md5($ip);
|
||||
$attempts = get_transient($transient_key);
|
||||
|
||||
if ($attempts === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return intval($attempts) >= $settings['rate_limit_orders'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an order attempt for rate limiting
|
||||
*
|
||||
* @param string|null $ip IP address (null = auto-detect)
|
||||
* @return void
|
||||
*/
|
||||
public static function record_order_attempt($ip = null)
|
||||
{
|
||||
$settings = self::get_settings();
|
||||
|
||||
if (!$settings['enable_checkout_rate_limit']) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($ip === null) {
|
||||
$ip = self::get_client_ip();
|
||||
}
|
||||
|
||||
$transient_key = 'woonoow_rate_' . md5($ip);
|
||||
$attempts = get_transient($transient_key);
|
||||
|
||||
if ($attempts === false) {
|
||||
// First attempt, set with expiration
|
||||
set_transient($transient_key, 1, $settings['rate_limit_minutes'] * MINUTE_IN_SECONDS);
|
||||
} else {
|
||||
// Increment attempts (keep same expiration by getting remaining time)
|
||||
$attempts = intval($attempts) + 1;
|
||||
set_transient($transient_key, $attempts, $settings['rate_limit_minutes'] * MINUTE_IN_SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate CAPTCHA token
|
||||
*
|
||||
* @param string $token CAPTCHA token from frontend
|
||||
* @return bool|WP_Error True if valid, WP_Error if invalid
|
||||
*/
|
||||
public static function validate_captcha($token)
|
||||
{
|
||||
$settings = self::get_settings();
|
||||
|
||||
if ($settings['captcha_provider'] === 'none') {
|
||||
return true; // No CAPTCHA enabled
|
||||
}
|
||||
|
||||
if (empty($token)) {
|
||||
return new \WP_Error('captcha_missing', __('CAPTCHA verification required', 'woonoow'));
|
||||
}
|
||||
|
||||
if ($settings['captcha_provider'] === 'recaptcha') {
|
||||
return self::validate_recaptcha($token, $settings['recaptcha_secret_key']);
|
||||
}
|
||||
|
||||
if ($settings['captcha_provider'] === 'turnstile') {
|
||||
return self::validate_turnstile($token, $settings['turnstile_secret_key']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Google reCAPTCHA v3 token
|
||||
*
|
||||
* @param string $token Token from frontend
|
||||
* @param string $secret_key Secret key
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
private static function validate_recaptcha($token, $secret_key)
|
||||
{
|
||||
if (empty($secret_key)) {
|
||||
return new \WP_Error('captcha_config', __('reCAPTCHA not configured', 'woonoow'));
|
||||
}
|
||||
|
||||
$response = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', [
|
||||
'body' => [
|
||||
'secret' => $secret_key,
|
||||
'response' => $token,
|
||||
'remoteip' => self::get_client_ip(),
|
||||
],
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new \WP_Error('captcha_error', __('CAPTCHA verification failed', 'woonoow'));
|
||||
}
|
||||
|
||||
$body = json_decode(wp_remote_retrieve_body($response), true);
|
||||
|
||||
if (!isset($body['success']) || !$body['success']) {
|
||||
return new \WP_Error('captcha_invalid', __('CAPTCHA verification failed', 'woonoow'));
|
||||
}
|
||||
|
||||
// reCAPTCHA v3 returns a score (0.0 - 1.0), we accept 0.5 and above
|
||||
$score = $body['score'] ?? 0;
|
||||
if ($score < 0.5) {
|
||||
return new \WP_Error('captcha_score', __('CAPTCHA score too low', 'woonoow'));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Cloudflare Turnstile token
|
||||
*
|
||||
* @param string $token Token from frontend
|
||||
* @param string $secret_key Secret key
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
private static function validate_turnstile($token, $secret_key)
|
||||
{
|
||||
if (empty($secret_key)) {
|
||||
return new \WP_Error('captcha_config', __('Turnstile not configured', 'woonoow'));
|
||||
}
|
||||
|
||||
$response = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
|
||||
'body' => [
|
||||
'secret' => $secret_key,
|
||||
'response' => $token,
|
||||
'remoteip' => self::get_client_ip(),
|
||||
],
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return new \WP_Error('captcha_error', __('CAPTCHA verification failed', 'woonoow'));
|
||||
}
|
||||
|
||||
$body = json_decode(wp_remote_retrieve_body($response), true);
|
||||
|
||||
if (!isset($body['success']) || !$body['success']) {
|
||||
return new \WP_Error('captcha_invalid', __('CAPTCHA verification failed', 'woonoow'));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client IP address
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function get_client_ip()
|
||||
{
|
||||
$headers = [
|
||||
'HTTP_CF_CONNECTING_IP', // Cloudflare
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_X_REAL_IP',
|
||||
'REMOTE_ADDR',
|
||||
];
|
||||
|
||||
foreach ($headers as $header) {
|
||||
if (!empty($_SERVER[$header])) {
|
||||
$ip = $_SERVER[$header];
|
||||
// Handle comma-separated list (X-Forwarded-For)
|
||||
if (strpos($ip, ',') !== false) {
|
||||
$ip = trim(explode(',', $ip)[0]);
|
||||
}
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '127.0.0.1';
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Campaign Manager
|
||||
*
|
||||
@@ -9,37 +10,43 @@
|
||||
|
||||
namespace WooNooW\Core\Campaigns;
|
||||
|
||||
use WooNooW\Database\SubscriberTable;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class CampaignManager {
|
||||
|
||||
class CampaignManager
|
||||
{
|
||||
|
||||
const POST_TYPE = 'wnw_campaign';
|
||||
const CRON_HOOK = 'woonoow_process_scheduled_campaigns';
|
||||
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
public static function instance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*/
|
||||
public static function init() {
|
||||
public static function init()
|
||||
{
|
||||
add_action('init', [__CLASS__, 'register_post_type']);
|
||||
add_action(self::CRON_HOOK, [__CLASS__, 'process_scheduled_campaigns']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Register campaign post type
|
||||
*/
|
||||
public static function register_post_type() {
|
||||
public static function register_post_type()
|
||||
{
|
||||
register_post_type(self::POST_TYPE, [
|
||||
'labels' => [
|
||||
'name' => __('Campaigns', 'woonoow'),
|
||||
@@ -53,32 +60,33 @@ class CampaignManager {
|
||||
'map_meta_cap' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new campaign
|
||||
*
|
||||
* @param array $data Campaign data
|
||||
* @return int|WP_Error Campaign ID or error
|
||||
*/
|
||||
public static function create($data) {
|
||||
public static function create($data)
|
||||
{
|
||||
$post_data = [
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'post_title' => sanitize_text_field($data['title'] ?? 'Untitled Campaign'),
|
||||
];
|
||||
|
||||
|
||||
$campaign_id = wp_insert_post($post_data, true);
|
||||
|
||||
|
||||
if (is_wp_error($campaign_id)) {
|
||||
return $campaign_id;
|
||||
}
|
||||
|
||||
|
||||
// Save meta fields
|
||||
self::update_meta($campaign_id, $data);
|
||||
|
||||
|
||||
return $campaign_id;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update campaign
|
||||
*
|
||||
@@ -86,13 +94,14 @@ class CampaignManager {
|
||||
* @param array $data Campaign data
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public static function update($campaign_id, $data) {
|
||||
public static function update($campaign_id, $data)
|
||||
{
|
||||
$post = get_post($campaign_id);
|
||||
|
||||
|
||||
if (!$post || $post->post_type !== self::POST_TYPE) {
|
||||
return new \WP_Error('invalid_campaign', __('Campaign not found', 'woonoow'));
|
||||
}
|
||||
|
||||
|
||||
// Update title if provided
|
||||
if (isset($data['title'])) {
|
||||
wp_update_post([
|
||||
@@ -100,31 +109,32 @@ class CampaignManager {
|
||||
'post_title' => sanitize_text_field($data['title']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// Update meta fields
|
||||
self::update_meta($campaign_id, $data);
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update campaign meta
|
||||
*
|
||||
* @param int $campaign_id
|
||||
* @param array $data
|
||||
*/
|
||||
private static function update_meta($campaign_id, $data) {
|
||||
private static function update_meta($campaign_id, $data)
|
||||
{
|
||||
$meta_fields = [
|
||||
'subject' => '_wnw_subject',
|
||||
'content' => '_wnw_content',
|
||||
'status' => '_wnw_status',
|
||||
'scheduled_at' => '_wnw_scheduled_at',
|
||||
];
|
||||
|
||||
|
||||
foreach ($meta_fields as $key => $meta_key) {
|
||||
if (isset($data[$key])) {
|
||||
$value = $data[$key];
|
||||
|
||||
|
||||
// Sanitize based on field type
|
||||
if ($key === 'content') {
|
||||
$value = wp_kses_post($value);
|
||||
@@ -136,40 +146,42 @@ class CampaignManager {
|
||||
} else {
|
||||
$value = sanitize_text_field($value);
|
||||
}
|
||||
|
||||
|
||||
update_post_meta($campaign_id, $meta_key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set default status if not provided
|
||||
if (!get_post_meta($campaign_id, '_wnw_status', true)) {
|
||||
update_post_meta($campaign_id, '_wnw_status', 'draft');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get campaign by ID
|
||||
*
|
||||
* @param int $campaign_id
|
||||
* @return array|null
|
||||
*/
|
||||
public static function get($campaign_id) {
|
||||
public static function get($campaign_id)
|
||||
{
|
||||
$post = get_post($campaign_id);
|
||||
|
||||
|
||||
if (!$post || $post->post_type !== self::POST_TYPE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return self::format_campaign($post);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all campaigns
|
||||
*
|
||||
* @param array $args Query args
|
||||
* @return array
|
||||
*/
|
||||
public static function get_all($args = []) {
|
||||
public static function get_all($args = [])
|
||||
{
|
||||
$defaults = [
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_status' => 'any',
|
||||
@@ -177,22 +189,23 @@ class CampaignManager {
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
];
|
||||
|
||||
|
||||
$query_args = wp_parse_args($args, $defaults);
|
||||
$query_args['post_type'] = self::POST_TYPE; // Force post type
|
||||
|
||||
|
||||
$posts = get_posts($query_args);
|
||||
|
||||
|
||||
return array_map([__CLASS__, 'format_campaign'], $posts);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format campaign post to array
|
||||
*
|
||||
* @param WP_Post $post
|
||||
* @return array
|
||||
*/
|
||||
private static function format_campaign($post) {
|
||||
private static function format_campaign($post)
|
||||
{
|
||||
return [
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
@@ -208,69 +221,71 @@ class CampaignManager {
|
||||
'updated_at' => $post->post_modified,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete campaign
|
||||
*
|
||||
* @param int $campaign_id
|
||||
* @return bool
|
||||
*/
|
||||
public static function delete($campaign_id) {
|
||||
public static function delete($campaign_id)
|
||||
{
|
||||
$post = get_post($campaign_id);
|
||||
|
||||
|
||||
if (!$post || $post->post_type !== self::POST_TYPE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return wp_delete_post($campaign_id, true) !== false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send campaign
|
||||
*
|
||||
* @param int $campaign_id
|
||||
* @return array Result with sent/failed counts
|
||||
*/
|
||||
public static function send($campaign_id) {
|
||||
public static function send($campaign_id)
|
||||
{
|
||||
$campaign = self::get($campaign_id);
|
||||
|
||||
|
||||
if (!$campaign) {
|
||||
return ['success' => false, 'error' => __('Campaign not found', 'woonoow')];
|
||||
}
|
||||
|
||||
|
||||
if ($campaign['status'] === 'sent') {
|
||||
return ['success' => false, 'error' => __('Campaign already sent', 'woonoow')];
|
||||
}
|
||||
|
||||
|
||||
// Get subscribers
|
||||
$subscribers = self::get_subscribers();
|
||||
|
||||
|
||||
if (empty($subscribers)) {
|
||||
return ['success' => false, 'error' => __('No subscribers to send to', 'woonoow')];
|
||||
}
|
||||
|
||||
|
||||
// Update status to sending
|
||||
update_post_meta($campaign_id, '_wnw_status', 'sending');
|
||||
update_post_meta($campaign_id, '_wnw_recipient_count', count($subscribers));
|
||||
|
||||
|
||||
$sent = 0;
|
||||
$failed = 0;
|
||||
|
||||
|
||||
// Get email template
|
||||
$template = self::render_campaign_email($campaign);
|
||||
|
||||
|
||||
// Send in batches
|
||||
$batch_size = 50;
|
||||
$batches = array_chunk($subscribers, $batch_size);
|
||||
|
||||
|
||||
foreach ($batches as $batch) {
|
||||
foreach ($batch as $subscriber) {
|
||||
$email = $subscriber['email'];
|
||||
|
||||
|
||||
// Replace subscriber-specific variables
|
||||
$body = str_replace('{subscriber_email}', $email, $template['body']);
|
||||
$body = str_replace('{unsubscribe_url}', self::get_unsubscribe_url($email), $body);
|
||||
|
||||
|
||||
// Send email
|
||||
$result = wp_mail(
|
||||
$email,
|
||||
@@ -278,26 +293,26 @@ class CampaignManager {
|
||||
$body,
|
||||
['Content-Type: text/html; charset=UTF-8']
|
||||
);
|
||||
|
||||
|
||||
if ($result) {
|
||||
$sent++;
|
||||
} else {
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Small delay between batches
|
||||
if (count($batches) > 1) {
|
||||
sleep(2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update campaign stats
|
||||
update_post_meta($campaign_id, '_wnw_sent_count', $sent);
|
||||
update_post_meta($campaign_id, '_wnw_failed_count', $failed);
|
||||
update_post_meta($campaign_id, '_wnw_sent_at', current_time('mysql'));
|
||||
update_post_meta($campaign_id, '_wnw_status', $failed > 0 && $sent === 0 ? 'failed' : 'sent');
|
||||
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'sent' => $sent,
|
||||
@@ -305,7 +320,7 @@ class CampaignManager {
|
||||
'total' => count($subscribers),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send test email
|
||||
*
|
||||
@@ -313,19 +328,20 @@ class CampaignManager {
|
||||
* @param string $email Test email address
|
||||
* @return bool
|
||||
*/
|
||||
public static function send_test($campaign_id, $email) {
|
||||
public static function send_test($campaign_id, $email)
|
||||
{
|
||||
$campaign = self::get($campaign_id);
|
||||
|
||||
|
||||
if (!$campaign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$template = self::render_campaign_email($campaign);
|
||||
|
||||
|
||||
// Replace subscriber-specific variables
|
||||
$body = str_replace('{subscriber_email}', $email, $template['body']);
|
||||
$body = str_replace('{unsubscribe_url}', '#', $body);
|
||||
|
||||
|
||||
return wp_mail(
|
||||
$email,
|
||||
'[TEST] ' . $template['subject'],
|
||||
@@ -333,43 +349,49 @@ class CampaignManager {
|
||||
['Content-Type: text/html; charset=UTF-8']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Render campaign email using EmailRenderer
|
||||
*
|
||||
* @param array $campaign
|
||||
* @return array ['subject' => string, 'body' => string]
|
||||
*/
|
||||
private static function render_campaign_email($campaign) {
|
||||
private static function render_campaign_email($campaign)
|
||||
{
|
||||
$renderer = \WooNooW\Core\Notifications\EmailRenderer::instance();
|
||||
|
||||
|
||||
// Get the campaign email template
|
||||
$template = $renderer->get_template_settings('newsletter_campaign', 'customer');
|
||||
|
||||
|
||||
// Fallback if no template configured
|
||||
if (!$template) {
|
||||
$subject = $campaign['subject'] ?: $campaign['title'];
|
||||
$body = $campaign['content'];
|
||||
} else {
|
||||
$subject = $template['subject'] ?: $campaign['subject'];
|
||||
|
||||
|
||||
// Replace {content} with campaign content
|
||||
$body = str_replace('{content}', $campaign['content'], $template['body']);
|
||||
|
||||
|
||||
// Replace {campaign_title}
|
||||
$body = str_replace('{campaign_title}', $campaign['title'], $body);
|
||||
}
|
||||
|
||||
|
||||
// Replace common variables
|
||||
$site_name = get_bloginfo('name');
|
||||
$site_url = home_url();
|
||||
|
||||
|
||||
// Replace campaign-specific variables in subject
|
||||
$subject = str_replace('{campaign_title}', $campaign['title'], $subject);
|
||||
$subject = str_replace(['{site_name}', '{store_name}'], $site_name, $subject);
|
||||
$body = str_replace(['{site_name}', '{store_name}'], $site_name, $body);
|
||||
$body = str_replace('{site_url}', $site_url, $body);
|
||||
$body = str_replace('{current_date}', date_i18n(get_option('date_format')), $body);
|
||||
$body = str_replace('{current_year}', date('Y'), $body);
|
||||
|
||||
|
||||
// Parse card shortcodes before rendering
|
||||
$body = $renderer->parse_cards($body);
|
||||
|
||||
// Render through email design template
|
||||
$design_path = $renderer->get_design_template();
|
||||
if (file_exists($design_path)) {
|
||||
@@ -378,69 +400,66 @@ class CampaignManager {
|
||||
'site_url' => $site_url,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
return [
|
||||
'subject' => $subject,
|
||||
'body' => $body,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get subscribers
|
||||
*
|
||||
* @param array $filters Optional audience filters
|
||||
* @return array
|
||||
*/
|
||||
private static function get_subscribers() {
|
||||
// Check if using custom table
|
||||
$use_table = !get_option('woonoow_newsletter_limit_enabled', true);
|
||||
|
||||
if ($use_table && self::has_subscribers_table()) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'woonoow_subscribers';
|
||||
return $wpdb->get_results(
|
||||
"SELECT email, user_id FROM {$table} WHERE status = 'active'",
|
||||
ARRAY_A
|
||||
);
|
||||
private static function get_subscribers($filters = [])
|
||||
{
|
||||
// Use SubscriberTable if available
|
||||
if (SubscriberTable::table_exists()) {
|
||||
return SubscriberTable::get_active($filters);
|
||||
}
|
||||
|
||||
// Use wp_options storage
|
||||
|
||||
// Legacy: use wp_options storage
|
||||
$subscribers = get_option('woonoow_newsletter_subscribers', []);
|
||||
return array_filter($subscribers, function($sub) {
|
||||
return array_filter($subscribers, function ($sub) {
|
||||
return ($sub['status'] ?? 'active') === 'active';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if subscribers table exists
|
||||
* Check if subscribers table exists (deprecated - use SubscriberTable::table_exists())
|
||||
*
|
||||
* @deprecated Use SubscriberTable::table_exists() instead
|
||||
* @return bool
|
||||
*/
|
||||
private static function has_subscribers_table() {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'woonoow_subscribers';
|
||||
return $wpdb->get_var("SHOW TABLES LIKE '{$table}'") === $table;
|
||||
private static function has_subscribers_table()
|
||||
{
|
||||
return SubscriberTable::table_exists();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get unsubscribe URL
|
||||
*
|
||||
* @param string $email
|
||||
* @return string
|
||||
*/
|
||||
private static function get_unsubscribe_url($email) {
|
||||
private static function get_unsubscribe_url($email)
|
||||
{
|
||||
// Use NewsletterController's secure token-based URL
|
||||
return \WooNooW\API\NewsletterController::generate_unsubscribe_url($email);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process scheduled campaigns (WP-Cron)
|
||||
*/
|
||||
public static function process_scheduled_campaigns() {
|
||||
public static function process_scheduled_campaigns()
|
||||
{
|
||||
// Only if scheduling is enabled
|
||||
if (!get_option('woonoow_campaign_scheduling_enabled', false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$campaigns = self::get_all([
|
||||
'meta_query' => [
|
||||
[
|
||||
@@ -455,25 +474,27 @@ class CampaignManager {
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
|
||||
foreach ($campaigns as $campaign) {
|
||||
self::send($campaign['id']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enable scheduling (registers cron)
|
||||
*/
|
||||
public static function enable_scheduling() {
|
||||
public static function enable_scheduling()
|
||||
{
|
||||
if (!wp_next_scheduled(self::CRON_HOOK)) {
|
||||
wp_schedule_event(time(), 'hourly', self::CRON_HOOK);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disable scheduling (clears cron)
|
||||
*/
|
||||
public static function disable_scheduling() {
|
||||
public static function disable_scheduling()
|
||||
{
|
||||
wp_clear_scheduled_hook(self::CRON_HOOK);
|
||||
}
|
||||
}
|
||||
|
||||
107
includes/Core/Notifications/ChannelRegistry.php
Normal file
107
includes/Core/Notifications/ChannelRegistry.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Channel Registry
|
||||
*
|
||||
* Manages registration and retrieval of notification channels
|
||||
*
|
||||
* @package WooNooW\Core\Notifications
|
||||
*/
|
||||
|
||||
namespace WooNooW\Core\Notifications;
|
||||
|
||||
use WooNooW\Core\Notifications\Channels\ChannelInterface;
|
||||
|
||||
class ChannelRegistry
|
||||
{
|
||||
|
||||
/**
|
||||
* Registered channels
|
||||
*
|
||||
* @var array<string, ChannelInterface>
|
||||
*/
|
||||
private static $channels = [];
|
||||
|
||||
/**
|
||||
* Register a notification channel
|
||||
*
|
||||
* @param ChannelInterface $channel Channel instance
|
||||
* @return bool Success status
|
||||
*/
|
||||
public static function register(ChannelInterface $channel)
|
||||
{
|
||||
$id = $channel->get_id();
|
||||
|
||||
if (empty($id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self::$channels[$id] = $channel;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a registered channel by ID
|
||||
*
|
||||
* @param string $channel_id Channel identifier
|
||||
* @return ChannelInterface|null Channel instance or null if not found
|
||||
*/
|
||||
public static function get($channel_id)
|
||||
{
|
||||
return self::$channels[$channel_id] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered channels
|
||||
*
|
||||
* @return array<string, ChannelInterface> Associative array of channel_id => channel_instance
|
||||
*/
|
||||
public static function get_all()
|
||||
{
|
||||
return self::$channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a channel is registered
|
||||
*
|
||||
* @param string $channel_id Channel identifier
|
||||
* @return bool True if channel exists
|
||||
*/
|
||||
public static function has($channel_id)
|
||||
{
|
||||
return isset(self::$channels[$channel_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a channel
|
||||
*
|
||||
* @param string $channel_id Channel identifier
|
||||
* @return bool Success status
|
||||
*/
|
||||
public static function unregister($channel_id)
|
||||
{
|
||||
if (isset(self::$channels[$channel_id])) {
|
||||
unset(self::$channels[$channel_id]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of configured channel IDs
|
||||
*
|
||||
* Only returns channels that are properly configured (is_configured() returns true)
|
||||
*
|
||||
* @return array List of configured channel IDs
|
||||
*/
|
||||
public static function get_configured_channels()
|
||||
{
|
||||
$configured = [];
|
||||
foreach (self::$channels as $id => $channel) {
|
||||
if ($channel->is_configured()) {
|
||||
$configured[] = $id;
|
||||
}
|
||||
}
|
||||
return $configured;
|
||||
}
|
||||
}
|
||||
57
includes/Core/Notifications/Channels/ChannelInterface.php
Normal file
57
includes/Core/Notifications/Channels/ChannelInterface.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Channel Interface
|
||||
*
|
||||
* Contract for implementing custom notification channels (WhatsApp, SMS, Telegram, etc.)
|
||||
*
|
||||
* @package WooNooW\Core\Notifications\Channels
|
||||
*/
|
||||
|
||||
namespace WooNooW\Core\Notifications\Channels;
|
||||
|
||||
interface ChannelInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Get channel unique identifier
|
||||
*
|
||||
* @return string Channel ID (e.g., 'whatsapp', 'sms', 'telegram')
|
||||
*/
|
||||
public function get_id();
|
||||
|
||||
/**
|
||||
* Get channel display label
|
||||
*
|
||||
* @return string Channel label for UI (e.g., 'WhatsApp', 'SMS', 'Telegram')
|
||||
*/
|
||||
public function get_label();
|
||||
|
||||
/**
|
||||
* Check if channel is properly configured
|
||||
*
|
||||
* Example: API keys are set, credentials are valid, etc.
|
||||
*
|
||||
* @return bool True if channel is ready to send notifications
|
||||
*/
|
||||
public function is_configured();
|
||||
|
||||
/**
|
||||
* Send notification through this channel
|
||||
*
|
||||
* @param string $event_id Event identifier (e.g., 'order_completed', 'newsletter_confirm')
|
||||
* @param string $recipient Recipient type ('customer', 'staff')
|
||||
* @param array $data Notification context data (order, user, custom vars, etc.)
|
||||
* @return bool|array Success status, or array with 'success' and 'message' keys
|
||||
*/
|
||||
public function send($event_id, $recipient, $data);
|
||||
|
||||
/**
|
||||
* Get channel configuration fields for admin settings
|
||||
*
|
||||
* Optional. Returns array of field definitions for settings UI.
|
||||
*
|
||||
* @return array Field definitions (e.g., API key, sender number, etc.)
|
||||
*/
|
||||
public function get_config_fields();
|
||||
}
|
||||
252
includes/Core/Notifications/Channels/WhatsAppChannel.example.php
Normal file
252
includes/Core/Notifications/Channels/WhatsAppChannel.example.php
Normal file
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* WhatsApp Channel - Example Implementation
|
||||
*
|
||||
* This is a reference implementation showing how to create a custom notification channel.
|
||||
* Developers can use this as a template for implementing WhatsApp, SMS, Telegram, etc.
|
||||
*
|
||||
* @package WooNooW\Core\Notifications\Channels
|
||||
*/
|
||||
|
||||
namespace WooNooW\Core\Notifications\Channels;
|
||||
|
||||
/**
|
||||
* Example WhatsApp Channel Implementation
|
||||
*
|
||||
* This channel sends notifications via WhatsApp Business API.
|
||||
* Replace API calls with your actual WhatsApp service provider (Twilio, MessageBird, etc.)
|
||||
*/
|
||||
class WhatsAppChannel implements ChannelInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Get channel ID
|
||||
*/
|
||||
public function get_id()
|
||||
{
|
||||
return 'whatsapp';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get channel label
|
||||
*/
|
||||
public function get_label()
|
||||
{
|
||||
return __('WhatsApp', 'woonoow');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if channel is configured
|
||||
*/
|
||||
public function is_configured()
|
||||
{
|
||||
$api_key = get_option('woonoow_whatsapp_api_key', '');
|
||||
$phone_number = get_option('woonoow_whatsapp_phone_number', '');
|
||||
|
||||
return !empty($api_key) && !empty($phone_number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send WhatsApp notification
|
||||
*
|
||||
* @param string $event_id Event identifier
|
||||
* @param string $recipient Recipient type ('customer' or 'staff')
|
||||
* @param array $data Context data (order, user, etc.)
|
||||
* @return bool|array Success status
|
||||
*/
|
||||
public function send($event_id, $recipient, $data)
|
||||
{
|
||||
// Get recipient phone number
|
||||
$phone = $this->get_recipient_phone($recipient, $data);
|
||||
|
||||
if (empty($phone)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'No phone number available for recipient',
|
||||
];
|
||||
}
|
||||
|
||||
// Build message content based on event
|
||||
$message = $this->build_message($event_id, $data);
|
||||
|
||||
if (empty($message)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Could not build message for event: ' . $event_id,
|
||||
];
|
||||
}
|
||||
|
||||
// Send via WhatsApp API
|
||||
$result = $this->send_whatsapp_message($phone, $message);
|
||||
|
||||
// Log the send attempt
|
||||
do_action('woonoow_whatsapp_sent', $event_id, $recipient, $phone, $result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration fields for admin settings
|
||||
*/
|
||||
public function get_config_fields()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'id' => 'woonoow_whatsapp_api_key',
|
||||
'label' => __('WhatsApp API Key', 'woonoow'),
|
||||
'type' => 'text',
|
||||
'description' => __('Your WhatsApp Business API key', 'woonoow'),
|
||||
],
|
||||
[
|
||||
'id' => 'woonoow_whatsapp_phone_number',
|
||||
'label' => __('WhatsApp Business Number', 'woonoow'),
|
||||
'type' => 'text',
|
||||
'description' => __('Your WhatsApp Business phone number (with country code)', 'woonoow'),
|
||||
'placeholder' => '+1234567890',
|
||||
],
|
||||
[
|
||||
'id' => 'woonoow_whatsapp_provider',
|
||||
'label' => __('Service Provider', 'woonoow'),
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
'twilio' => 'Twilio',
|
||||
'messagebird' => 'MessageBird',
|
||||
'custom' => 'Custom',
|
||||
],
|
||||
'default' => 'twilio',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recipient phone number
|
||||
*
|
||||
* @param string $recipient Recipient type
|
||||
* @param array $data Context data
|
||||
* @return string Phone number or empty string
|
||||
*/
|
||||
private function get_recipient_phone($recipient, $data)
|
||||
{
|
||||
if ($recipient === 'customer') {
|
||||
// Get customer phone from order or user data
|
||||
if (isset($data['order'])) {
|
||||
return $data['order']->get_billing_phone();
|
||||
}
|
||||
if (isset($data['user_id'])) {
|
||||
return get_user_meta($data['user_id'], 'billing_phone', true);
|
||||
}
|
||||
if (isset($data['email'])) {
|
||||
$user = get_user_by('email', $data['email']);
|
||||
if ($user) {
|
||||
return get_user_meta($user->ID, 'billing_phone', true);
|
||||
}
|
||||
}
|
||||
} elseif ($recipient === 'staff') {
|
||||
// Get admin phone from settings
|
||||
return get_option('woonoow_whatsapp_admin_phone', '');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build message content based on event
|
||||
*
|
||||
* @param string $event_id Event identifier
|
||||
* @param array $data Context data
|
||||
* @return string Message text
|
||||
*/
|
||||
private function build_message($event_id, $data)
|
||||
{
|
||||
// Allow filtering message content
|
||||
$message = apply_filters("woonoow_whatsapp_message_{$event_id}", '', $data);
|
||||
|
||||
if (!empty($message)) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
// Default messages for common events
|
||||
$site_name = get_bloginfo('name');
|
||||
|
||||
switch ($event_id) {
|
||||
case 'order_completed':
|
||||
if (isset($data['order'])) {
|
||||
$order = $data['order'];
|
||||
return sprintf(
|
||||
"🎉 Your order #%s has been completed! Thank you for shopping with %s.",
|
||||
$order->get_order_number(),
|
||||
$site_name
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'newsletter_confirm':
|
||||
if (isset($data['confirmation_url'])) {
|
||||
return sprintf(
|
||||
"Please confirm your newsletter subscription by clicking: %s",
|
||||
$data['confirmation_url']
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
// Add more event templates as needed
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Send WhatsApp message via API
|
||||
*
|
||||
* Replace this with actual API integration for your provider
|
||||
*
|
||||
* @param string $phone Recipient phone number
|
||||
* @param string $message Message text
|
||||
* @return array Result with 'success' and 'message' keys
|
||||
*/
|
||||
private function send_whatsapp_message($phone, $message)
|
||||
{
|
||||
$api_key = get_option('woonoow_whatsapp_api_key', '');
|
||||
$from_number = get_option('woonoow_whatsapp_phone_number', '');
|
||||
$provider = get_option('woonoow_whatsapp_provider', 'twilio');
|
||||
|
||||
// Example: Twilio API (replace with your actual implementation)
|
||||
if ($provider === 'twilio') {
|
||||
$endpoint = 'https://api.twilio.com/2010-04-01/Accounts/YOUR_ACCOUNT_SID/Messages.json';
|
||||
|
||||
$response = wp_remote_post($endpoint, [
|
||||
'headers' => [
|
||||
'Authorization' => 'Basic ' . base64_encode($api_key),
|
||||
'Content-Type' => 'application/x-www-form-urlencoded',
|
||||
],
|
||||
'body' => [
|
||||
'From' => 'whatsapp:' . $from_number,
|
||||
'To' => 'whatsapp:' . $phone,
|
||||
'Body' => $message,
|
||||
],
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $response->get_error_message(),
|
||||
];
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code($response);
|
||||
return [
|
||||
'success' => $status_code >= 200 && $status_code < 300,
|
||||
'message' => $status_code >= 200 && $status_code < 300
|
||||
? 'WhatsApp message sent successfully'
|
||||
: 'Failed to send WhatsApp message',
|
||||
];
|
||||
}
|
||||
|
||||
// For custom providers, implement your own logic here
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Provider not configured',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Default Email Templates (DEPRECATED)
|
||||
*
|
||||
@@ -17,8 +18,9 @@ namespace WooNooW\Core\Notifications;
|
||||
|
||||
use WooNooW\Email\DefaultTemplates as NewDefaultTemplates;
|
||||
|
||||
class DefaultEmailTemplates {
|
||||
|
||||
class DefaultEmailTemplates
|
||||
{
|
||||
|
||||
/**
|
||||
* Get default template for an event and recipient type
|
||||
*
|
||||
@@ -26,28 +28,30 @@ class DefaultEmailTemplates {
|
||||
* @param string $recipient_type 'staff' or 'customer'
|
||||
* @return array ['subject' => string, 'body' => string]
|
||||
*/
|
||||
public static function get_template($event_id, $recipient_type) {
|
||||
public static function get_template($event_id, $recipient_type)
|
||||
{
|
||||
// Get templates directly from this class
|
||||
$allTemplates = self::get_all_templates();
|
||||
|
||||
|
||||
// Check if event exists for this recipient type
|
||||
if (isset($allTemplates[$event_id][$recipient_type])) {
|
||||
return $allTemplates[$event_id][$recipient_type];
|
||||
}
|
||||
|
||||
|
||||
// Fallback
|
||||
return [
|
||||
'subject' => __('Notification from {store_name}', 'woonoow'),
|
||||
'body' => '[card]' . __('You have a new notification.', 'woonoow') . '[/card]',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all default templates (legacy method - kept for backwards compatibility)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function get_all_templates() {
|
||||
private static function get_all_templates()
|
||||
{
|
||||
// This method is now deprecated but kept for backwards compatibility
|
||||
// Use WooNooW\Email\DefaultTemplates instead
|
||||
return [
|
||||
@@ -83,7 +87,7 @@ class DefaultEmailTemplates {
|
||||
[button url="{order_url}" style="solid"]' . __('View Order Details', 'woonoow') . '[/button]',
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
'order_processing' => [
|
||||
'customer' => [
|
||||
'subject' => __('Your Order #{order_number} is Being Processed', 'woonoow'),
|
||||
@@ -112,7 +116,7 @@ class DefaultEmailTemplates {
|
||||
[button url="{order_url}" style="solid"]' . __('Track Your Order', 'woonoow') . '[/button]',
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
'order_completed' => [
|
||||
'customer' => [
|
||||
'subject' => __('Your Order #{order_number} is Complete', 'woonoow'),
|
||||
@@ -135,10 +139,10 @@ class DefaultEmailTemplates {
|
||||
[/card]
|
||||
|
||||
[button url="{order_url}" style="solid"]' . __('View Order', 'woonoow') . '[/button]
|
||||
[button url="{store_url}" style="outline"]' . __('Continue Shopping', 'woonoow') . '[/button]',
|
||||
[button url="{shop_url}" style="outline"]' . __('Continue Shopping', 'woonoow') . '[/button]',
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
'order_cancelled' => [
|
||||
'staff' => [
|
||||
'subject' => __('Order #{order_number} Cancelled', 'woonoow'),
|
||||
@@ -158,7 +162,7 @@ class DefaultEmailTemplates {
|
||||
[button url="{order_url}" style="solid"]' . __('View Order Details', 'woonoow') . '[/button]',
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
'order_refunded' => [
|
||||
'customer' => [
|
||||
'subject' => __('Your Order #{order_number} Has Been Refunded', 'woonoow'),
|
||||
@@ -183,7 +187,7 @@ class DefaultEmailTemplates {
|
||||
[button url="{order_url}" style="solid"]' . __('View Order', 'woonoow') . '[/button]',
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
// PRODUCT EVENTS
|
||||
'low_stock' => [
|
||||
'staff' => [
|
||||
@@ -209,7 +213,7 @@ class DefaultEmailTemplates {
|
||||
[button url="{product_url}" style="solid"]' . __('View Product', 'woonoow') . '[/button]',
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
'out_of_stock' => [
|
||||
'staff' => [
|
||||
'subject' => __('Out of Stock Alert: {product_name}', 'woonoow'),
|
||||
@@ -233,7 +237,7 @@ class DefaultEmailTemplates {
|
||||
[button url="{product_url}" style="solid"]' . __('Manage Product', 'woonoow') . '[/button]',
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
// CUSTOMER EVENTS
|
||||
'new_customer' => [
|
||||
'customer' => [
|
||||
@@ -261,10 +265,10 @@ class DefaultEmailTemplates {
|
||||
[/card]
|
||||
|
||||
[button url="{account_url}" style="solid"]' . __('Go to My Account', 'woonoow') . '[/button]
|
||||
[button url="{store_url}" style="outline"]' . __('Start Shopping', 'woonoow') . '[/button]',
|
||||
[button url="{shop_url}" style="outline"]' . __('Start Shopping', 'woonoow') . '[/button]',
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
'customer_note' => [
|
||||
'customer' => [
|
||||
'subject' => __('Note Added to Your Order #{order_number}', 'woonoow'),
|
||||
@@ -289,16 +293,17 @@ class DefaultEmailTemplates {
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all new templates (direct access to new class)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_new_templates() {
|
||||
public static function get_new_templates()
|
||||
{
|
||||
return NewDefaultTemplates::get_all_templates();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get default subject from new templates
|
||||
*
|
||||
@@ -306,7 +311,8 @@ class DefaultEmailTemplates {
|
||||
* @param string $event_id Event ID
|
||||
* @return string
|
||||
*/
|
||||
public static function get_default_subject($recipient_type, $event_id) {
|
||||
public static function get_default_subject($recipient_type, $event_id)
|
||||
{
|
||||
return NewDefaultTemplates::get_default_subject($recipient_type, $event_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class EmailRenderer
|
||||
* @param string $recipient_type
|
||||
* @return array|null
|
||||
*/
|
||||
private function get_template_settings($event_id, $recipient_type)
|
||||
public function get_template_settings($event_id, $recipient_type)
|
||||
{
|
||||
// Get saved template (with recipient_type for proper default template lookup)
|
||||
$template = TemplateProvider::get_template($event_id, 'email', $recipient_type);
|
||||
@@ -187,7 +187,7 @@ class EmailRenderer
|
||||
'site_name' => get_bloginfo('name'),
|
||||
'site_title' => 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'),
|
||||
@@ -381,7 +381,7 @@ class EmailRenderer
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
private function parse_cards($content)
|
||||
public function parse_cards($content)
|
||||
{
|
||||
// Use a single unified regex to match BOTH syntaxes in document order
|
||||
// This ensures cards are rendered in the order they appear
|
||||
@@ -473,8 +473,31 @@ class EmailRenderer
|
||||
$hero_text_color = '#ffffff'; // Always white on gradient
|
||||
|
||||
// Parse button shortcodes with FULL INLINE STYLES for Gmail compatibility
|
||||
// Helper function to escape URL while preserving variable placeholders like {unsubscribe_url}
|
||||
$escape_url_preserving_variables = function ($url) {
|
||||
// If URL contains variable placeholder, don't escape (will be replaced later)
|
||||
if (preg_match('/\{[a-z_]+\}/', $url)) {
|
||||
// Just return the URL as-is - it will be replaced with a real URL later
|
||||
return $url;
|
||||
}
|
||||
return esc_url($url);
|
||||
};
|
||||
|
||||
// Helper function to generate button HTML
|
||||
$generateButtonHtml = function ($url, $style, $text) use ($primary_color, $secondary_color, $button_text_color) {
|
||||
$generateButtonHtml = function ($url, $style, $text) use ($primary_color, $secondary_color, $button_text_color, $escape_url_preserving_variables) {
|
||||
$escaped_url = $escape_url_preserving_variables($url);
|
||||
|
||||
if ($style === 'link') {
|
||||
// Plain link - just a simple <a> tag styled like regular text link (inline, no wrapper)
|
||||
return sprintf(
|
||||
'<a href="%s" style="color: %s; text-decoration: underline; font-family: \'Inter\', Arial, sans-serif;">%s</a>',
|
||||
$escaped_url,
|
||||
esc_attr($primary_color),
|
||||
esc_html($text)
|
||||
);
|
||||
}
|
||||
|
||||
// Styled buttons (solid/outline) get table wrapper for email client compatibility
|
||||
if ($style === 'outline') {
|
||||
// Outline button - transparent background with border
|
||||
$button_style = sprintf(
|
||||
@@ -494,7 +517,7 @@ class EmailRenderer
|
||||
// Use table-based button for better email client compatibility
|
||||
return sprintf(
|
||||
'<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="margin: 16px auto;"><tr><td align="center"><a href="%s" style="%s">%s</a></td></tr></table>',
|
||||
esc_url($url),
|
||||
$escaped_url,
|
||||
$button_style,
|
||||
esc_html($text)
|
||||
);
|
||||
@@ -542,9 +565,25 @@ class EmailRenderer
|
||||
$content_style .= sprintf(' color: %s;', esc_attr($hero_text_color));
|
||||
|
||||
// Add inline color to all headings and paragraphs for email client compatibility
|
||||
$content = preg_replace(
|
||||
'/<(h[1-6]|p)([^>]*)>/',
|
||||
'<$1$2 style="color: ' . esc_attr($hero_text_color) . ';">',
|
||||
// Preserve existing style attributes (like text-align) by appending to them
|
||||
$content = preg_replace_callback(
|
||||
'/<(h[1-6]|p)([^>]*?)(\s+style=["\']([^"\']*)["\'])?([^>]*)>/',
|
||||
function ($matches) use ($hero_text_color) {
|
||||
$tag = $matches[1];
|
||||
$before_style = $matches[2];
|
||||
$existing_style = isset($matches[4]) ? $matches[4] : '';
|
||||
$after_style = $matches[5];
|
||||
$color_style = 'color: ' . esc_attr($hero_text_color) . ';';
|
||||
|
||||
if ($existing_style) {
|
||||
// Append to existing style
|
||||
$new_style = rtrim($existing_style, ';') . '; ' . $color_style;
|
||||
return '<' . $tag . $before_style . ' style="' . $new_style . '"' . $after_style . '>';
|
||||
} else {
|
||||
// Add new style attribute
|
||||
return '<' . $tag . $before_style . ' style="' . $color_style . '"' . $after_style . '>';
|
||||
}
|
||||
},
|
||||
$content
|
||||
);
|
||||
}
|
||||
@@ -560,6 +599,11 @@ class EmailRenderer
|
||||
elseif ($type === 'warning') {
|
||||
$style .= ' background-color: #fff8e1;';
|
||||
}
|
||||
// Basic card - plain text, no card styling (for footers/muted content)
|
||||
elseif ($type === 'basic') {
|
||||
$style = 'width: 100%; background-color: transparent;'; // No background
|
||||
$content_style = 'padding: 0;'; // No padding
|
||||
}
|
||||
}
|
||||
|
||||
// Add background image
|
||||
@@ -616,7 +660,7 @@ class EmailRenderer
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_design_template()
|
||||
public function get_design_template()
|
||||
{
|
||||
// Use single base template (theme-agnostic)
|
||||
$template_path = WOONOOW_PATH . 'templates/emails/base.html';
|
||||
@@ -641,7 +685,7 @@ class EmailRenderer
|
||||
* @param array $variables All variables
|
||||
* @return string
|
||||
*/
|
||||
private function render_html($template_path, $content, $subject, $variables)
|
||||
public function render_html($template_path, $content, $subject, $variables)
|
||||
{
|
||||
if (!file_exists($template_path)) {
|
||||
// Fallback to plain HTML
|
||||
@@ -654,6 +698,10 @@ class EmailRenderer
|
||||
// Get email customization settings
|
||||
$email_settings = get_option('woonoow_email_settings', []);
|
||||
|
||||
// Ensure required variables have defaults
|
||||
$variables['site_url'] = $variables['site_url'] ?? home_url();
|
||||
$variables['store_name'] = $variables['store_name'] ?? get_bloginfo('name');
|
||||
|
||||
// Email body background
|
||||
$body_bg = '#f8f8f8';
|
||||
|
||||
@@ -668,7 +716,7 @@ class EmailRenderer
|
||||
if (!empty($logo_url)) {
|
||||
$header = sprintf(
|
||||
'<a href="%s"><img src="%s" alt="%s" style="max-width: 200px; max-height: 60px;"></a>',
|
||||
esc_url($variables['store_url']),
|
||||
esc_url($variables['site_url']),
|
||||
esc_url($logo_url),
|
||||
esc_attr($variables['store_name'])
|
||||
);
|
||||
@@ -677,7 +725,7 @@ class EmailRenderer
|
||||
$header_text = !empty($email_settings['header_text']) ? $email_settings['header_text'] : $variables['store_name'];
|
||||
$header = sprintf(
|
||||
'<a href="%s" style="font-size: 24px; font-weight: 700; color: #333; text-decoration: none;">%s</a>',
|
||||
esc_url($variables['store_url']),
|
||||
esc_url($variables['site_url']),
|
||||
esc_html($header_text)
|
||||
);
|
||||
}
|
||||
@@ -724,7 +772,7 @@ class EmailRenderer
|
||||
$html = str_replace('{{email_content}}', $content, $html);
|
||||
$html = str_replace('{{email_footer}}', $footer, $html);
|
||||
$html = str_replace('{{store_name}}', esc_html($variables['store_name']), $html);
|
||||
$html = str_replace('{{store_url}}', esc_url($variables['store_url']), $html);
|
||||
$html = str_replace('{{site_url}}', esc_url($variables['site_url']), $html);
|
||||
$html = str_replace('{{current_year}}', date('Y'), $html);
|
||||
|
||||
// Replace all other variables
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Markdown to Email HTML Parser
|
||||
*
|
||||
@@ -17,21 +18,23 @@
|
||||
|
||||
namespace WooNooW\Core\Notifications;
|
||||
|
||||
class MarkdownParser {
|
||||
|
||||
class MarkdownParser
|
||||
{
|
||||
|
||||
/**
|
||||
* Parse markdown to email HTML
|
||||
*
|
||||
* @param string $markdown
|
||||
* @return string
|
||||
*/
|
||||
public static function parse($markdown) {
|
||||
public static function parse($markdown)
|
||||
{
|
||||
$html = $markdown;
|
||||
|
||||
|
||||
// Parse card blocks first (:::card or :::card[type])
|
||||
$html = preg_replace_callback(
|
||||
'/:::card(?:\[(\w+)\])?\n([\s\S]*?):::/s',
|
||||
function($matches) {
|
||||
function ($matches) {
|
||||
$type = $matches[1] ?? '';
|
||||
$content = trim($matches[2]);
|
||||
$parsed_content = self::parse_basics($content);
|
||||
@@ -39,12 +42,12 @@ class MarkdownParser {
|
||||
},
|
||||
$html
|
||||
);
|
||||
|
||||
|
||||
// Parse button blocks [button url="..."]Text[/button] - already in correct format
|
||||
// Also support legacy [button](url){text} syntax
|
||||
$html = preg_replace_callback(
|
||||
'/\[button(?:\s+style="(solid|outline)")?\]\((.*?)\)\s*\{([^}]+)\}/',
|
||||
function($matches) {
|
||||
function ($matches) {
|
||||
$style = $matches[1] ?? '';
|
||||
$url = $matches[2];
|
||||
$text = $matches[3];
|
||||
@@ -52,71 +55,88 @@ class MarkdownParser {
|
||||
},
|
||||
$html
|
||||
);
|
||||
|
||||
|
||||
// Horizontal rules
|
||||
$html = preg_replace('/^---$/m', '<hr>', $html);
|
||||
|
||||
|
||||
// Parse remaining markdown (outside cards)
|
||||
$html = self::parse_basics($html);
|
||||
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse basic markdown syntax
|
||||
*
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
private static function parse_basics($text) {
|
||||
private static function parse_basics($text)
|
||||
{
|
||||
$html = $text;
|
||||
|
||||
|
||||
// Protect variables from markdown parsing by temporarily replacing them
|
||||
$variables = [];
|
||||
$var_index = 0;
|
||||
$html = preg_replace_callback('/\{([^}]+)\}/', function($matches) use (&$variables, &$var_index) {
|
||||
$html = preg_replace_callback('/\{([^}]+)\}/', function ($matches) use (&$variables, &$var_index) {
|
||||
$placeholder = '<!--VAR' . $var_index . '-->';
|
||||
$variables[$placeholder] = $matches[0];
|
||||
$var_index++;
|
||||
return $placeholder;
|
||||
}, $html);
|
||||
|
||||
|
||||
// Protect existing HTML tags (h1-h6, p) with style attributes from being overwritten
|
||||
$html_tags = [];
|
||||
$tag_index = 0;
|
||||
$html = preg_replace_callback('/<(h[1-6]|p)([^>]*style=[^>]*)>/', function ($matches) use (&$html_tags, &$tag_index) {
|
||||
$placeholder = '<!--HTMLTAG' . $tag_index . '-->';
|
||||
$html_tags[$placeholder] = $matches[0];
|
||||
$tag_index++;
|
||||
return $placeholder;
|
||||
}, $html);
|
||||
|
||||
// Headings (must be done in order from h4 to h1 to avoid conflicts)
|
||||
// Only match markdown syntax (lines starting with #), not existing HTML
|
||||
$html = preg_replace('/^#### (.*)$/m', '<h4>$1</h4>', $html);
|
||||
$html = preg_replace('/^### (.*)$/m', '<h3>$1</h3>', $html);
|
||||
$html = preg_replace('/^## (.*)$/m', '<h2>$1</h2>', $html);
|
||||
$html = preg_replace('/^# (.*)$/m', '<h1>$1</h1>', $html);
|
||||
|
||||
|
||||
// Restore protected HTML tags
|
||||
foreach ($html_tags as $placeholder => $original) {
|
||||
$html = str_replace($placeholder, $original, $html);
|
||||
}
|
||||
|
||||
// Bold (don't match across newlines)
|
||||
$html = preg_replace('/\*\*([^\n*]+?)\*\*/', '<strong>$1</strong>', $html);
|
||||
$html = preg_replace('/__([^\n_]+?)__/', '<strong>$1</strong>', $html);
|
||||
|
||||
|
||||
// Italic (don't match across newlines)
|
||||
$html = preg_replace('/\*([^\n*]+?)\*/', '<em>$1</em>', $html);
|
||||
$html = preg_replace('/_([^\n_]+?)_/', '<em>$1</em>', $html);
|
||||
|
||||
|
||||
// Horizontal rules
|
||||
$html = preg_replace('/^---$/m', '<hr>', $html);
|
||||
|
||||
|
||||
// Links (but not button syntax)
|
||||
$html = preg_replace('/\[(?!button)([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $html);
|
||||
|
||||
|
||||
// Process lines for paragraphs and lists
|
||||
$lines = explode("\n", $html);
|
||||
$in_list = false;
|
||||
$paragraph_content = '';
|
||||
$processed_lines = [];
|
||||
|
||||
$close_paragraph = function() use (&$paragraph_content, &$processed_lines) {
|
||||
|
||||
$close_paragraph = function () use (&$paragraph_content, &$processed_lines) {
|
||||
if ($paragraph_content) {
|
||||
$processed_lines[] = '<p>' . $paragraph_content . '</p>';
|
||||
$paragraph_content = '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$trimmed = trim($line);
|
||||
|
||||
|
||||
// Empty line - close paragraph or list
|
||||
if (empty($trimmed)) {
|
||||
if ($in_list) {
|
||||
@@ -127,7 +147,7 @@ class MarkdownParser {
|
||||
$processed_lines[] = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Check if line is a list item
|
||||
if (preg_match('/^[\*\-•✓✔]\s/', $trimmed)) {
|
||||
$close_paragraph();
|
||||
@@ -139,20 +159,20 @@ class MarkdownParser {
|
||||
$processed_lines[] = '<li>' . $content . '</li>';
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Close list if we're in one
|
||||
if ($in_list) {
|
||||
$processed_lines[] = '</ul>';
|
||||
$in_list = false;
|
||||
}
|
||||
|
||||
|
||||
// Block-level HTML tags - don't wrap in paragraph
|
||||
if (preg_match('/^<(div|h1|h2|h3|h4|h5|h6|p|ul|ol|li|hr|table|blockquote)/i', $trimmed)) {
|
||||
$close_paragraph();
|
||||
$processed_lines[] = $line;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Regular text line - accumulate in paragraph
|
||||
if ($paragraph_content) {
|
||||
// Add line break before continuation (THIS IS THE KEY FIX!)
|
||||
@@ -162,30 +182,31 @@ class MarkdownParser {
|
||||
$paragraph_content = $trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Close any open tags
|
||||
if ($in_list) {
|
||||
$processed_lines[] = '</ul>';
|
||||
}
|
||||
$close_paragraph();
|
||||
|
||||
|
||||
$html = implode("\n", $processed_lines);
|
||||
|
||||
|
||||
// Restore variables
|
||||
foreach ($variables as $placeholder => $original) {
|
||||
$html = str_replace($placeholder, $original, $html);
|
||||
}
|
||||
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert newlines to <br> tags for email rendering
|
||||
*
|
||||
* @param string $html
|
||||
* @return string
|
||||
*/
|
||||
public static function nl2br_email($html) {
|
||||
public static function nl2br_email($html)
|
||||
{
|
||||
// Don't convert newlines inside HTML tags
|
||||
$html = preg_replace('/(?<!>)\n(?!<)/', '<br>', $html);
|
||||
return $html;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Notification Manager
|
||||
*
|
||||
@@ -9,32 +10,43 @@
|
||||
|
||||
namespace WooNooW\Core\Notifications;
|
||||
|
||||
class NotificationManager {
|
||||
|
||||
use WooNooW\Core\Notifications\ChannelRegistry;
|
||||
|
||||
class NotificationManager
|
||||
{
|
||||
|
||||
/**
|
||||
* Check if a channel is enabled globally
|
||||
*
|
||||
* @param string $channel_id Channel ID (email, push, etc.)
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_channel_enabled($channel_id) {
|
||||
public static function is_channel_enabled($channel_id)
|
||||
{
|
||||
// Check built-in channels
|
||||
if ($channel_id === 'email') {
|
||||
return (bool) get_option('woonoow_email_notifications_enabled', true);
|
||||
} elseif ($channel_id === 'push') {
|
||||
return (bool) get_option('woonoow_push_notifications_enabled', true);
|
||||
}
|
||||
|
||||
// For addon channels, check if they're registered and enabled
|
||||
|
||||
// Check if channel is registered in ChannelRegistry
|
||||
if (ChannelRegistry::has($channel_id)) {
|
||||
$channel = ChannelRegistry::get($channel_id);
|
||||
return $channel->is_configured();
|
||||
}
|
||||
|
||||
// Legacy: check via filter (backward compatibility)
|
||||
$channels = apply_filters('woonoow_notification_channels', []);
|
||||
foreach ($channels as $channel) {
|
||||
if ($channel['id'] === $channel_id) {
|
||||
return isset($channel['enabled']) ? (bool) $channel['enabled'] : true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if a channel is enabled for a specific event
|
||||
*
|
||||
@@ -42,24 +54,25 @@ class NotificationManager {
|
||||
* @param string $channel_id Channel ID
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_event_channel_enabled($event_id, $channel_id) {
|
||||
public static function is_event_channel_enabled($event_id, $channel_id)
|
||||
{
|
||||
$settings = get_option('woonoow_notification_settings', []);
|
||||
|
||||
|
||||
if (!isset($settings[$event_id])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$event = $settings[$event_id];
|
||||
|
||||
|
||||
if (!isset($event['channels'][$channel_id])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isset($event['channels'][$channel_id]['enabled'])
|
||||
? (bool) $event['channels'][$channel_id]['enabled']
|
||||
|
||||
return isset($event['channels'][$channel_id]['enabled'])
|
||||
? (bool) $event['channels'][$channel_id]['enabled']
|
||||
: false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if notification should be sent
|
||||
*
|
||||
@@ -69,26 +82,27 @@ class NotificationManager {
|
||||
* @param string $channel_id Channel ID
|
||||
* @return bool
|
||||
*/
|
||||
public static function should_send_notification($event_id, $channel_id) {
|
||||
public static function should_send_notification($event_id, $channel_id)
|
||||
{
|
||||
// Check if WooNooW notification system is enabled
|
||||
$system_mode = get_option('woonoow_notification_system_mode', 'woonoow');
|
||||
if ($system_mode !== 'woonoow') {
|
||||
return false; // Use WooCommerce default emails instead
|
||||
}
|
||||
|
||||
|
||||
// Check if channel is globally enabled
|
||||
if (!self::is_channel_enabled($channel_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Check if channel is enabled for this specific event
|
||||
if (!self::is_event_channel_enabled($event_id, $channel_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get recipient for event channel
|
||||
*
|
||||
@@ -96,16 +110,17 @@ class NotificationManager {
|
||||
* @param string $channel_id Channel ID
|
||||
* @return string Recipient type (admin, customer, both)
|
||||
*/
|
||||
public static function get_recipient($event_id, $channel_id) {
|
||||
public static function get_recipient($event_id, $channel_id)
|
||||
{
|
||||
$settings = get_option('woonoow_notification_settings', []);
|
||||
|
||||
|
||||
if (!isset($settings[$event_id]['channels'][$channel_id]['recipient'])) {
|
||||
return 'admin';
|
||||
}
|
||||
|
||||
|
||||
return $settings[$event_id]['channels'][$channel_id]['recipient'];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send notification through specified channel
|
||||
*
|
||||
@@ -114,16 +129,25 @@ class NotificationManager {
|
||||
* @param array $data Notification data
|
||||
* @return bool Success status
|
||||
*/
|
||||
public static function send($event_id, $channel_id, $data = []) {
|
||||
public static function send($event_id, $channel_id, $data = [])
|
||||
{
|
||||
// Validate if notification should be sent
|
||||
if (!self::should_send_notification($event_id, $channel_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Get recipient
|
||||
$recipient = self::get_recipient($event_id, $channel_id);
|
||||
|
||||
// Allow addons to handle their own channels
|
||||
|
||||
// Try to use registered channel from ChannelRegistry
|
||||
if (ChannelRegistry::has($channel_id)) {
|
||||
$channel = ChannelRegistry::get($channel_id);
|
||||
if ($channel->is_configured()) {
|
||||
return $channel->send($event_id, $recipient, $data);
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy: Allow addons to handle their own channels via filter
|
||||
$sent = apply_filters(
|
||||
'woonoow_send_notification',
|
||||
false,
|
||||
@@ -132,22 +156,22 @@ class NotificationManager {
|
||||
$recipient,
|
||||
$data
|
||||
);
|
||||
|
||||
|
||||
// If addon handled it, return
|
||||
if ($sent !== false) {
|
||||
return $sent;
|
||||
}
|
||||
|
||||
// Handle built-in channels
|
||||
|
||||
// Handle built-in channels (email, push)
|
||||
if ($channel_id === 'email') {
|
||||
return self::send_email($event_id, $recipient, $data);
|
||||
} elseif ($channel_id === 'push') {
|
||||
return self::send_push($event_id, $recipient, $data);
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send email notification
|
||||
*
|
||||
@@ -156,25 +180,26 @@ class NotificationManager {
|
||||
* @param array $data Notification data
|
||||
* @return bool
|
||||
*/
|
||||
private static function send_email($event_id, $recipient, $data) {
|
||||
private static function send_email($event_id, $recipient, $data)
|
||||
{
|
||||
// Use EmailRenderer to render the email
|
||||
$renderer = EmailRenderer::instance();
|
||||
$email_data = $renderer->render($event_id, $recipient, $data['order'] ?? $data['product'] ?? $data['customer'] ?? null, $data);
|
||||
|
||||
|
||||
if (!$email_data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Send email using wp_mail
|
||||
$headers = ['Content-Type: text/html; charset=UTF-8'];
|
||||
$sent = wp_mail($email_data['to'], $email_data['subject'], $email_data['body'], $headers);
|
||||
|
||||
|
||||
// Trigger action for logging/tracking
|
||||
do_action('woonoow_email_sent', $event_id, $recipient, $email_data, $sent);
|
||||
|
||||
|
||||
return $sent;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send push notification
|
||||
*
|
||||
@@ -183,7 +208,8 @@ class NotificationManager {
|
||||
* @param array $data Notification data
|
||||
* @return bool
|
||||
*/
|
||||
private static function send_push($event_id, $recipient, $data) {
|
||||
private static function send_push($event_id, $recipient, $data)
|
||||
{
|
||||
// Push notification sending will be implemented later
|
||||
// This is a placeholder for future implementation
|
||||
do_action('woonoow_send_push_notification', $event_id, $recipient, $data);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Notification Template Provider
|
||||
*
|
||||
@@ -11,27 +12,29 @@ namespace WooNooW\Core\Notifications;
|
||||
|
||||
use WooNooW\Email\DefaultTemplates as EmailDefaultTemplates;
|
||||
|
||||
class TemplateProvider {
|
||||
|
||||
class TemplateProvider
|
||||
{
|
||||
|
||||
/**
|
||||
* Option key for storing templates
|
||||
*/
|
||||
const OPTION_KEY = 'woonoow_notification_templates';
|
||||
|
||||
|
||||
/**
|
||||
* Get all templates
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_templates() {
|
||||
public static function get_templates()
|
||||
{
|
||||
$templates = get_option(self::OPTION_KEY, []);
|
||||
|
||||
|
||||
// Merge with defaults
|
||||
$defaults = self::get_default_templates();
|
||||
|
||||
|
||||
return array_merge($defaults, $templates);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get template for specific event and channel
|
||||
*
|
||||
@@ -40,25 +43,26 @@ class TemplateProvider {
|
||||
* @param string $recipient_type Recipient type ('customer' or 'staff')
|
||||
* @return array|null
|
||||
*/
|
||||
public static function get_template($event_id, $channel_id, $recipient_type = 'customer') {
|
||||
public static function get_template($event_id, $channel_id, $recipient_type = 'customer')
|
||||
{
|
||||
$templates = self::get_templates();
|
||||
|
||||
|
||||
$key = "{$recipient_type}_{$event_id}_{$channel_id}";
|
||||
|
||||
|
||||
if (isset($templates[$key])) {
|
||||
return $templates[$key];
|
||||
}
|
||||
|
||||
|
||||
// Return default if exists
|
||||
$defaults = self::get_default_templates();
|
||||
|
||||
|
||||
if (isset($defaults[$key])) {
|
||||
return $defaults[$key];
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save template
|
||||
*
|
||||
@@ -68,11 +72,12 @@ class TemplateProvider {
|
||||
* @param string $recipient_type Recipient type ('customer' or 'staff')
|
||||
* @return bool
|
||||
*/
|
||||
public static function save_template($event_id, $channel_id, $template, $recipient_type = 'customer') {
|
||||
public static function save_template($event_id, $channel_id, $template, $recipient_type = 'customer')
|
||||
{
|
||||
$templates = get_option(self::OPTION_KEY, []);
|
||||
|
||||
|
||||
$key = "{$recipient_type}_{$event_id}_{$channel_id}";
|
||||
|
||||
|
||||
$templates[$key] = [
|
||||
'event_id' => $event_id,
|
||||
'channel_id' => $channel_id,
|
||||
@@ -82,10 +87,10 @@ class TemplateProvider {
|
||||
'variables' => $template['variables'] ?? [],
|
||||
'updated_at' => current_time('mysql'),
|
||||
];
|
||||
|
||||
|
||||
return update_option(self::OPTION_KEY, $templates);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete template (revert to default)
|
||||
*
|
||||
@@ -94,46 +99,48 @@ class TemplateProvider {
|
||||
* @param string $recipient_type Recipient type ('customer' or 'staff')
|
||||
* @return bool
|
||||
*/
|
||||
public static function delete_template($event_id, $channel_id, $recipient_type = 'customer') {
|
||||
public static function delete_template($event_id, $channel_id, $recipient_type = 'customer')
|
||||
{
|
||||
$templates = get_option(self::OPTION_KEY, []);
|
||||
|
||||
|
||||
$key = "{$recipient_type}_{$event_id}_{$channel_id}";
|
||||
|
||||
|
||||
if (isset($templates[$key])) {
|
||||
unset($templates[$key]);
|
||||
return update_option(self::OPTION_KEY, $templates);
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get default templates
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_default_templates() {
|
||||
public static function get_default_templates()
|
||||
{
|
||||
$templates = [];
|
||||
|
||||
|
||||
// Get all events from EventRegistry (single source of truth)
|
||||
$all_events = EventRegistry::get_all_events();
|
||||
|
||||
|
||||
// Get email templates from DefaultTemplates
|
||||
$allEmailTemplates = EmailDefaultTemplates::get_all_templates();
|
||||
|
||||
|
||||
foreach ($all_events as $event) {
|
||||
$event_id = $event['id'];
|
||||
$recipient_type = $event['recipient_type'];
|
||||
// Get template body from the new clean markdown source
|
||||
$body = $allEmailTemplates[$recipient_type][$event_id] ?? '';
|
||||
$subject = EmailDefaultTemplates::get_default_subject($recipient_type, $event_id);
|
||||
|
||||
|
||||
// If template doesn't exist, create a simple fallback
|
||||
if (empty($body)) {
|
||||
$body = "[card]\n\n## Notification\n\nYou have a new notification about {$event_id}.\n\n[/card]";
|
||||
$subject = __('Notification from {store_name}', 'woonoow');
|
||||
}
|
||||
|
||||
|
||||
$templates["{$recipient_type}_{$event_id}_email"] = [
|
||||
'event_id' => $event_id,
|
||||
'channel_id' => 'email',
|
||||
@@ -143,7 +150,7 @@ class TemplateProvider {
|
||||
'variables' => self::get_variables_for_event($event_id),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// Add push notification templates
|
||||
$templates['staff_order_placed_push'] = [
|
||||
'event_id' => 'order_placed',
|
||||
@@ -217,42 +224,44 @@ class TemplateProvider {
|
||||
'body' => __('A note has been added to order #{order_number}', 'woonoow'),
|
||||
'variables' => self::get_order_variables(),
|
||||
];
|
||||
|
||||
|
||||
return $templates;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get variables for a specific event
|
||||
*
|
||||
* @param string $event_id Event ID
|
||||
* @return array
|
||||
*/
|
||||
private static function get_variables_for_event($event_id) {
|
||||
private static function get_variables_for_event($event_id)
|
||||
{
|
||||
// Product events
|
||||
if (in_array($event_id, ['low_stock', 'out_of_stock'])) {
|
||||
return self::get_product_variables();
|
||||
}
|
||||
|
||||
|
||||
// Customer events (but not order-related)
|
||||
if ($event_id === 'new_customer') {
|
||||
return self::get_customer_variables();
|
||||
}
|
||||
|
||||
|
||||
// Subscription events
|
||||
if (strpos($event_id, 'subscription_') === 0) {
|
||||
return self::get_subscription_variables();
|
||||
}
|
||||
|
||||
|
||||
// All other events are order-related
|
||||
return self::get_order_variables();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get available order variables
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_order_variables() {
|
||||
public static function get_order_variables()
|
||||
{
|
||||
return [
|
||||
'order_number' => __('Order Number', 'woonoow'),
|
||||
'order_total' => __('Order Total', 'woonoow'),
|
||||
@@ -272,49 +281,52 @@ class TemplateProvider {
|
||||
'billing_address' => __('Billing Address', 'woonoow'),
|
||||
'shipping_address' => __('Shipping Address', 'woonoow'),
|
||||
'store_name' => __('Store Name', 'woonoow'),
|
||||
'store_url' => __('Store URL', 'woonoow'),
|
||||
'site_url' => __('Site URL', 'woonoow'),
|
||||
'store_email' => __('Store Email', 'woonoow'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get available product variables
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_product_variables() {
|
||||
public static function get_product_variables()
|
||||
{
|
||||
return [
|
||||
'product_name' => __('Product Name', 'woonoow'),
|
||||
'product_sku' => __('Product SKU', 'woonoow'),
|
||||
'product_url' => __('Product URL', 'woonoow'),
|
||||
'stock_quantity' => __('Stock Quantity', 'woonoow'),
|
||||
'store_name' => __('Store Name', 'woonoow'),
|
||||
'store_url' => __('Store URL', 'woonoow'),
|
||||
'site_url' => __('Site URL', 'woonoow'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get available customer variables
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_customer_variables() {
|
||||
public static function get_customer_variables()
|
||||
{
|
||||
return [
|
||||
'customer_name' => __('Customer Name', 'woonoow'),
|
||||
'customer_email' => __('Customer Email', 'woonoow'),
|
||||
'customer_phone' => __('Customer Phone', 'woonoow'),
|
||||
'store_name' => __('Store Name', 'woonoow'),
|
||||
'store_url' => __('Store URL', 'woonoow'),
|
||||
'site_url' => __('Site URL', 'woonoow'),
|
||||
'store_email' => __('Store Email', 'woonoow'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get available subscription variables
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_subscription_variables() {
|
||||
public static function get_subscription_variables()
|
||||
{
|
||||
return [
|
||||
'subscription_id' => __('Subscription ID', 'woonoow'),
|
||||
'subscription_status' => __('Subscription Status', 'woonoow'),
|
||||
@@ -327,11 +339,11 @@ class TemplateProvider {
|
||||
'customer_name' => __('Customer Name', 'woonoow'),
|
||||
'customer_email' => __('Customer Email', 'woonoow'),
|
||||
'store_name' => __('Store Name', 'woonoow'),
|
||||
'store_url' => __('Store URL', 'woonoow'),
|
||||
'site_url' => __('Site URL', 'woonoow'),
|
||||
'my_account_url' => __('My Account URL', 'woonoow'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Replace variables in template
|
||||
*
|
||||
@@ -339,11 +351,12 @@ class TemplateProvider {
|
||||
* @param array $data Data to replace variables
|
||||
* @return string
|
||||
*/
|
||||
public static function replace_variables($content, $data) {
|
||||
public static function replace_variables($content, $data)
|
||||
{
|
||||
foreach ($data as $key => $value) {
|
||||
$content = str_replace('{' . $key . '}', $value, $content);
|
||||
}
|
||||
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
366
includes/Database/SubscriberTable.php
Normal file
366
includes/Database/SubscriberTable.php
Normal file
@@ -0,0 +1,366 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Subscriber Table - Custom database table for newsletter subscribers
|
||||
*
|
||||
* Provides scalable storage for subscribers instead of wp_options
|
||||
*
|
||||
* @package WooNooW\Database
|
||||
*/
|
||||
|
||||
namespace WooNooW\Database;
|
||||
|
||||
class SubscriberTable
|
||||
{
|
||||
|
||||
const TABLE_NAME = 'woonoow_subscribers';
|
||||
const DB_VERSION = '1.0';
|
||||
const DB_VERSION_OPTION = 'woonoow_subscribers_db_version';
|
||||
|
||||
/**
|
||||
* Get full table name with prefix
|
||||
*/
|
||||
public static function get_table_name()
|
||||
{
|
||||
global $wpdb;
|
||||
return $wpdb->prefix . self::TABLE_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update the subscribers table
|
||||
*/
|
||||
public static function create_table()
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$table_name = self::get_table_name();
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE $table_name (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
user_id BIGINT(20) UNSIGNED DEFAULT NULL,
|
||||
status ENUM('pending','active','unsubscribed') DEFAULT 'pending',
|
||||
consent TINYINT(1) DEFAULT 0,
|
||||
consent_text TEXT,
|
||||
source VARCHAR(50) DEFAULT 'form',
|
||||
subscribed_at DATETIME DEFAULT NULL,
|
||||
confirmed_at DATETIME DEFAULT NULL,
|
||||
unsubscribed_at DATETIME DEFAULT NULL,
|
||||
ip_address VARCHAR(45) DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY idx_email (email),
|
||||
KEY idx_status (status),
|
||||
KEY idx_user_id (user_id)
|
||||
) $charset_collate;";
|
||||
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
dbDelta($sql);
|
||||
|
||||
update_option(self::DB_VERSION_OPTION, self::DB_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if table exists
|
||||
*/
|
||||
public static function table_exists()
|
||||
{
|
||||
global $wpdb;
|
||||
$table_name = self::get_table_name();
|
||||
return $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate existing subscribers from wp_options to custom table
|
||||
*
|
||||
* @return array Migration result with counts
|
||||
*/
|
||||
public static function migrate_from_options()
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
// Ensure table exists
|
||||
if (!self::table_exists()) {
|
||||
self::create_table();
|
||||
}
|
||||
|
||||
$table_name = self::get_table_name();
|
||||
$subscribers = get_option('woonoow_newsletter_subscribers', []);
|
||||
|
||||
if (empty($subscribers)) {
|
||||
return ['migrated' => 0, 'skipped' => 0, 'message' => 'No subscribers to migrate'];
|
||||
}
|
||||
|
||||
$migrated = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($subscribers as $sub) {
|
||||
if (empty($sub['email'])) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if already exists in table
|
||||
$exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM $table_name WHERE email = %s",
|
||||
$sub['email']
|
||||
));
|
||||
|
||||
if ($exists) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Insert into new table
|
||||
$result = $wpdb->insert($table_name, [
|
||||
'email' => $sub['email'],
|
||||
'user_id' => $sub['user_id'] ?? null,
|
||||
'status' => $sub['status'] ?? 'active',
|
||||
'consent' => !empty($sub['consent']) ? 1 : 0,
|
||||
'subscribed_at' => $sub['subscribed_at'] ?? current_time('mysql'),
|
||||
'confirmed_at' => $sub['confirmed_at'] ?? null,
|
||||
'ip_address' => $sub['ip_address'] ?? null,
|
||||
]);
|
||||
|
||||
if ($result) {
|
||||
$migrated++;
|
||||
} else {
|
||||
$skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
// If all migrated successfully, remove the option
|
||||
if ($migrated > 0 && $skipped === 0) {
|
||||
delete_option('woonoow_newsletter_subscribers');
|
||||
}
|
||||
|
||||
return [
|
||||
'migrated' => $migrated,
|
||||
'skipped' => $skipped,
|
||||
'message' => "Migrated $migrated subscribers, skipped $skipped",
|
||||
];
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CRUD OPERATIONS
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Add a new subscriber
|
||||
*/
|
||||
public static function add($data)
|
||||
{
|
||||
global $wpdb;
|
||||
$table_name = self::get_table_name();
|
||||
|
||||
$result = $wpdb->insert($table_name, [
|
||||
'email' => $data['email'],
|
||||
'user_id' => $data['user_id'] ?? null,
|
||||
'status' => $data['status'] ?? 'pending',
|
||||
'consent' => !empty($data['consent']) ? 1 : 0,
|
||||
'consent_text' => $data['consent_text'] ?? null,
|
||||
'source' => $data['source'] ?? 'form',
|
||||
'subscribed_at' => $data['subscribed_at'] ?? current_time('mysql'),
|
||||
'ip_address' => $data['ip_address'] ?? null,
|
||||
]);
|
||||
|
||||
if ($result) {
|
||||
return $wpdb->insert_id;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a subscriber by email
|
||||
*/
|
||||
public static function get_by_email($email)
|
||||
{
|
||||
global $wpdb;
|
||||
$table_name = self::get_table_name();
|
||||
|
||||
return $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE email = %s",
|
||||
$email
|
||||
), ARRAY_A);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a subscriber by ID
|
||||
*/
|
||||
public static function get($id)
|
||||
{
|
||||
global $wpdb;
|
||||
$table_name = self::get_table_name();
|
||||
|
||||
return $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE id = %d",
|
||||
$id
|
||||
), ARRAY_A);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a subscriber
|
||||
*/
|
||||
public static function update($id, $data)
|
||||
{
|
||||
global $wpdb;
|
||||
$table_name = self::get_table_name();
|
||||
|
||||
return $wpdb->update($table_name, $data, ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update subscriber by email
|
||||
*/
|
||||
public static function update_by_email($email, $data)
|
||||
{
|
||||
global $wpdb;
|
||||
$table_name = self::get_table_name();
|
||||
|
||||
return $wpdb->update($table_name, $data, ['email' => $email]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a subscriber
|
||||
*/
|
||||
public static function delete($id)
|
||||
{
|
||||
global $wpdb;
|
||||
$table_name = self::get_table_name();
|
||||
|
||||
return $wpdb->delete($table_name, ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete subscriber by email
|
||||
*/
|
||||
public static function delete_by_email($email)
|
||||
{
|
||||
global $wpdb;
|
||||
$table_name = self::get_table_name();
|
||||
|
||||
return $wpdb->delete($table_name, ['email' => $email]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active subscribers
|
||||
*
|
||||
* @param array $filters Optional filters
|
||||
* @return array List of subscribers
|
||||
*/
|
||||
public static function get_active($filters = [])
|
||||
{
|
||||
global $wpdb;
|
||||
$table_name = self::get_table_name();
|
||||
|
||||
$where = ["status = 'active'"];
|
||||
$values = [];
|
||||
|
||||
// Filter: subscribed after date
|
||||
if (!empty($filters['subscribed_after'])) {
|
||||
$where[] = "subscribed_at >= %s";
|
||||
$values[] = $filters['subscribed_after'];
|
||||
}
|
||||
|
||||
// Filter: subscribed before date
|
||||
if (!empty($filters['subscribed_before'])) {
|
||||
$where[] = "subscribed_at <= %s";
|
||||
$values[] = $filters['subscribed_before'];
|
||||
}
|
||||
|
||||
// Filter: registered users only
|
||||
if (!empty($filters['registered_only'])) {
|
||||
$where[] = "user_id IS NOT NULL";
|
||||
}
|
||||
|
||||
// Filter: guests only
|
||||
if (!empty($filters['guests_only'])) {
|
||||
$where[] = "user_id IS NULL";
|
||||
}
|
||||
|
||||
$where_sql = implode(' AND ', $where);
|
||||
$sql = "SELECT * FROM $table_name WHERE $where_sql ORDER BY subscribed_at DESC";
|
||||
|
||||
if (!empty($values)) {
|
||||
$sql = $wpdb->prepare($sql, ...$values);
|
||||
}
|
||||
|
||||
return $wpdb->get_results($sql, ARRAY_A);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all subscribers with pagination
|
||||
*/
|
||||
public static function get_all($args = [])
|
||||
{
|
||||
global $wpdb;
|
||||
$table_name = self::get_table_name();
|
||||
|
||||
$defaults = [
|
||||
'per_page' => 20,
|
||||
'page' => 1,
|
||||
'status' => '',
|
||||
'search' => '',
|
||||
'orderby' => 'subscribed_at',
|
||||
'order' => 'DESC',
|
||||
];
|
||||
|
||||
$args = wp_parse_args($args, $defaults);
|
||||
$where = [];
|
||||
$values = [];
|
||||
|
||||
if (!empty($args['status'])) {
|
||||
$where[] = "status = %s";
|
||||
$values[] = $args['status'];
|
||||
}
|
||||
|
||||
if (!empty($args['search'])) {
|
||||
$where[] = "email LIKE %s";
|
||||
$values[] = '%' . $wpdb->esc_like($args['search']) . '%';
|
||||
}
|
||||
|
||||
$where_sql = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
// Get total count
|
||||
$count_sql = "SELECT COUNT(*) FROM $table_name $where_sql";
|
||||
if (!empty($values)) {
|
||||
$count_sql = $wpdb->prepare($count_sql, ...$values);
|
||||
}
|
||||
$total = (int) $wpdb->get_var($count_sql);
|
||||
|
||||
// Get paginated results
|
||||
$offset = ($args['page'] - 1) * $args['per_page'];
|
||||
$orderby = sanitize_sql_orderby($args['orderby'] . ' ' . $args['order']) ?: 'subscribed_at DESC';
|
||||
|
||||
$sql = "SELECT * FROM $table_name $where_sql ORDER BY $orderby LIMIT %d OFFSET %d";
|
||||
$values[] = $args['per_page'];
|
||||
$values[] = $offset;
|
||||
|
||||
$items = $wpdb->get_results($wpdb->prepare($sql, ...$values), ARRAY_A);
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'pages' => ceil($total / $args['per_page']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Count subscribers by status
|
||||
*/
|
||||
public static function count_by_status($status = null)
|
||||
{
|
||||
global $wpdb;
|
||||
$table_name = self::get_table_name();
|
||||
|
||||
if ($status) {
|
||||
return (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM $table_name WHERE status = %s",
|
||||
$status
|
||||
));
|
||||
}
|
||||
|
||||
return (int) $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace WooNooW\Frontend;
|
||||
|
||||
/**
|
||||
* Frontend Assets Manager
|
||||
* Handles loading of customer-spa assets
|
||||
*/
|
||||
class Assets {
|
||||
|
||||
class Assets
|
||||
{
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*/
|
||||
public static function init() {
|
||||
public static function init()
|
||||
{
|
||||
add_action('wp_enqueue_scripts', [self::class, 'enqueue_assets'], 20);
|
||||
add_action('wp_head', [self::class, 'add_inline_config'], 5);
|
||||
add_action('wp_enqueue_scripts', [self::class, 'dequeue_conflicting_scripts'], 100);
|
||||
add_filter('script_loader_tag', [self::class, 'add_module_type'], 10, 3);
|
||||
add_action('woocommerce_before_main_content', [self::class, 'inject_spa_mount_point'], 5);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add type="module" to customer-spa scripts
|
||||
*/
|
||||
public static function add_module_type($tag, $handle, $src) {
|
||||
public static function add_module_type($tag, $handle, $src)
|
||||
{
|
||||
// Add type="module" to our Vite scripts
|
||||
if (strpos($handle, 'woonoow-customer') !== false) {
|
||||
$tag = str_replace('<script ', '<script type="module" ', $tag);
|
||||
}
|
||||
return $tag;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enqueue customer-spa assets
|
||||
*/
|
||||
public static function enqueue_assets() {
|
||||
public static function enqueue_assets()
|
||||
{
|
||||
// Only load on pages with WooNooW shortcodes or in full SPA mode
|
||||
if (!self::should_load_assets()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if dev mode is enabled
|
||||
$is_dev = defined('WOONOOW_CUSTOMER_DEV') && WOONOOW_CUSTOMER_DEV;
|
||||
|
||||
|
||||
if ($is_dev) {
|
||||
// Dev mode: Load from Vite dev server
|
||||
$dev_server = 'https://woonoow.local:5174';
|
||||
|
||||
|
||||
// Vite client for HMR
|
||||
wp_enqueue_script(
|
||||
'woonoow-customer-vite',
|
||||
@@ -53,7 +58,7 @@ class Assets {
|
||||
null,
|
||||
false // Load in header
|
||||
);
|
||||
|
||||
|
||||
// Main entry point
|
||||
wp_enqueue_script(
|
||||
'woonoow-customer-spa',
|
||||
@@ -66,16 +71,16 @@ class Assets {
|
||||
// Production mode: Load from build
|
||||
$plugin_url = plugin_dir_url(dirname(dirname(__FILE__)));
|
||||
$dist_path = plugin_dir_path(dirname(dirname(__FILE__))) . 'customer-spa/dist/';
|
||||
|
||||
|
||||
// Check if build exists
|
||||
if (!file_exists($dist_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Production build - load app.js and app.css directly
|
||||
$js_url = $plugin_url . 'customer-spa/dist/app.js';
|
||||
$css_url = $plugin_url . 'customer-spa/dist/app.css';
|
||||
|
||||
|
||||
wp_enqueue_script(
|
||||
'woonoow-customer-spa',
|
||||
$js_url,
|
||||
@@ -83,15 +88,15 @@ class Assets {
|
||||
null,
|
||||
true
|
||||
);
|
||||
|
||||
|
||||
// Add type="module" for Vite build
|
||||
add_filter('script_loader_tag', function($tag, $handle, $src) {
|
||||
add_filter('script_loader_tag', function ($tag, $handle, $src) {
|
||||
if ($handle === 'woonoow-customer-spa') {
|
||||
$tag = str_replace('<script ', '<script type="module" ', $tag);
|
||||
}
|
||||
return $tag;
|
||||
}, 10, 3);
|
||||
|
||||
|
||||
wp_enqueue_style(
|
||||
'woonoow-customer-spa',
|
||||
$css_url,
|
||||
@@ -100,33 +105,35 @@ class Assets {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inject SPA mounting point for full mode
|
||||
*/
|
||||
public static function inject_spa_mount_point() {
|
||||
public static function inject_spa_mount_point()
|
||||
{
|
||||
if (!self::should_load_assets()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if we're in full mode and not on a page with shortcode
|
||||
$spa_settings = get_option('woonoow_customer_spa_settings', []);
|
||||
$mode = isset($spa_settings['mode']) ? $spa_settings['mode'] : 'disabled';
|
||||
|
||||
|
||||
if ($mode === 'full') {
|
||||
// Only inject if the mount point doesn't already exist (from shortcode)
|
||||
echo '<div id="woonoow-customer-app" data-page="shop"><div class="woonoow-loading"><p>Loading...</p></div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add inline config and scripts to page head
|
||||
*/
|
||||
public static function add_inline_config() {
|
||||
public static function add_inline_config()
|
||||
{
|
||||
if (!self::should_load_assets()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get Customer SPA settings
|
||||
$spa_settings = get_option('woonoow_customer_spa_settings', []);
|
||||
$default_settings = [
|
||||
@@ -142,14 +149,14 @@ class Assets {
|
||||
],
|
||||
];
|
||||
$theme_settings = array_replace_recursive($default_settings, $spa_settings);
|
||||
|
||||
|
||||
// Get appearance settings and preload them
|
||||
$stored_settings = get_option('woonoow_appearance_settings', []);
|
||||
$default_appearance = \WooNooW\Admin\AppearanceController::get_default_settings();
|
||||
|
||||
|
||||
// Merge stored settings with defaults to ensure new fields (like gradient colors) exist
|
||||
$appearance_settings = array_replace_recursive($default_appearance, $stored_settings);
|
||||
|
||||
|
||||
// Get WooCommerce currency settings
|
||||
$currency_settings = [
|
||||
'code' => get_woocommerce_currency(),
|
||||
@@ -159,16 +166,16 @@ class Assets {
|
||||
'decimalSeparator' => wc_get_price_decimal_separator(),
|
||||
'decimals' => wc_get_price_decimals(),
|
||||
];
|
||||
|
||||
|
||||
// Get store logo from WooNooW Store Details (Settings > Store Details)
|
||||
$logo_url = get_option('woonoow_store_logo', '');
|
||||
|
||||
|
||||
// Get user billing/shipping data if logged in
|
||||
$user_data = [
|
||||
'isLoggedIn' => is_user_logged_in(),
|
||||
'id' => get_current_user_id(),
|
||||
];
|
||||
|
||||
|
||||
if (is_user_logged_in()) {
|
||||
$customer = new \WC_Customer(get_current_user_id());
|
||||
$user_data['email'] = $customer->get_email();
|
||||
@@ -193,15 +200,15 @@ class Assets {
|
||||
'country' => $customer->get_shipping_country(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// Determine SPA base path for BrowserRouter
|
||||
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
|
||||
$spa_page = $spa_page_id ? get_post($spa_page_id) : null;
|
||||
|
||||
|
||||
// Check if SPA Entry Page is set as WordPress frontpage
|
||||
$frontpage_id = (int) get_option('page_on_front');
|
||||
$is_spa_wp_frontpage = $frontpage_id && $spa_page_id && $frontpage_id === (int) $spa_page_id;
|
||||
|
||||
|
||||
// Get SPA Landing page (explicit setting, separate from Entry Page)
|
||||
// This determines what content to show at the SPA root route "/"
|
||||
$spa_frontpage_id = $appearance_settings['general']['spa_frontpage'] ?? 0;
|
||||
@@ -211,14 +218,18 @@ class Assets {
|
||||
if ($spa_frontpage) {
|
||||
$front_page_slug = $spa_frontpage->post_name;
|
||||
}
|
||||
} elseif ($is_spa_wp_frontpage && $spa_page) {
|
||||
// If the SPA Entry Page itself is set as the WP Frontpage,
|
||||
// use its content as the SPA Frontpage content.
|
||||
$front_page_slug = $spa_page->post_name;
|
||||
}
|
||||
|
||||
|
||||
// If SPA Entry Page is WP frontpage, base path is /, otherwise use Entry Page slug
|
||||
$base_path = $is_spa_wp_frontpage ? '' : ($spa_page ? '/' . $spa_page->post_name : '/store');
|
||||
|
||||
|
||||
// Check if BrowserRouter is enabled (default: true for SEO)
|
||||
$use_browser_router = $appearance_settings['general']['use_browser_router'] ?? true;
|
||||
|
||||
|
||||
$config = [
|
||||
'apiUrl' => rest_url('woonoow/v1'),
|
||||
'apiRoot' => rest_url('woonoow/v1'),
|
||||
@@ -236,19 +247,20 @@ class Assets {
|
||||
'useBrowserRouter' => $use_browser_router,
|
||||
'frontPageSlug' => $front_page_slug,
|
||||
'spaMode' => $appearance_settings['general']['spa_mode'] ?? 'full',
|
||||
'security' => \WooNooW\Compat\SecuritySettingsProvider::get_public_settings(),
|
||||
];
|
||||
|
||||
?>
|
||||
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
window.woonoowCustomer = <?php echo wp_json_encode($config); ?>;
|
||||
</script>
|
||||
<?php
|
||||
|
||||
|
||||
// If dev mode, output scripts directly
|
||||
$is_dev = defined('WOONOOW_CUSTOMER_DEV') && WOONOOW_CUSTOMER_DEV;
|
||||
if ($is_dev) {
|
||||
$dev_server = 'https://woonoow.local:5174';
|
||||
?>
|
||||
?>
|
||||
<script type="module">
|
||||
import RefreshRuntime from '<?php echo $dev_server; ?>/@react-refresh'
|
||||
RefreshRuntime.injectIntoGlobalHook(window)
|
||||
@@ -258,35 +270,36 @@ class Assets {
|
||||
</script>
|
||||
<script type="module" crossorigin src="<?php echo $dev_server; ?>/@vite/client"></script>
|
||||
<script type="module" crossorigin src="<?php echo $dev_server; ?>/src/main.tsx"></script>
|
||||
<?php
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if we should load customer-spa assets
|
||||
*/
|
||||
private static function should_load_assets() {
|
||||
private static function should_load_assets()
|
||||
{
|
||||
global $post;
|
||||
|
||||
|
||||
// Check if we're serving SPA directly (set by serve_spa_for_frontpage_routes)
|
||||
if (defined('WOONOOW_SERVE_SPA') && WOONOOW_SERVE_SPA) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check if we're on a frontpage SPA route (by URL detection)
|
||||
if (self::is_frontpage_spa_route()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// First check: Is this a designated SPA page?
|
||||
if (self::is_spa_page()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Get SPA mode from appearance settings (the correct source)
|
||||
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||
$mode = $appearance_settings['general']['spa_mode'] ?? 'full';
|
||||
|
||||
|
||||
// If disabled, only load for pages with shortcodes
|
||||
if ($mode === 'disabled') {
|
||||
// Special handling for WooCommerce Shop page (it's an archive, not a regular post)
|
||||
@@ -299,7 +312,7 @@ class Assets {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check for shortcodes on regular pages
|
||||
if ($post) {
|
||||
if (has_shortcode($post->post_content, 'woonoow_shop')) {
|
||||
@@ -317,7 +330,7 @@ class Assets {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Full SPA mode - load on all WooCommerce pages
|
||||
if ($mode === 'full') {
|
||||
if (function_exists('is_shop') && is_shop()) {
|
||||
@@ -337,11 +350,11 @@ class Assets {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Checkout-Only mode - load only on specific pages
|
||||
if ($mode === 'checkout_only') {
|
||||
$checkout_pages = isset($spa_settings['checkoutPages']) ? $spa_settings['checkoutPages'] : [];
|
||||
|
||||
|
||||
if (!empty($checkout_pages['checkout']) && function_exists('is_checkout') && is_checkout() && !is_order_received_page()) {
|
||||
return true;
|
||||
}
|
||||
@@ -356,7 +369,7 @@ class Assets {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Check if current page has WooNooW shortcodes
|
||||
if ($post && has_shortcode($post->post_content, 'woonoow_shop')) {
|
||||
return true;
|
||||
@@ -370,65 +383,67 @@ class Assets {
|
||||
if ($post && has_shortcode($post->post_content, 'woonoow_account')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if current page is the designated SPA page
|
||||
*/
|
||||
private static function is_spa_page() {
|
||||
private static function is_spa_page()
|
||||
{
|
||||
global $post;
|
||||
if (!$post) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Get SPA page ID from appearance settings
|
||||
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||
$spa_page_id = isset($appearance_settings['general']['spa_page']) ? $appearance_settings['general']['spa_page'] : 0;
|
||||
|
||||
|
||||
// Check if current page matches the SPA page
|
||||
if ($spa_page_id && $post->ID == $spa_page_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if current request is a frontpage SPA route
|
||||
* Used to detect SPA routes by URL when SPA page is set as frontpage
|
||||
*/
|
||||
private static function is_frontpage_spa_route() {
|
||||
private static function is_frontpage_spa_route()
|
||||
{
|
||||
// Get SPA settings
|
||||
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
|
||||
$spa_mode = $appearance_settings['general']['spa_mode'] ?? 'full';
|
||||
|
||||
|
||||
// Only run in full SPA mode
|
||||
if ($spa_mode !== 'full' || !$spa_page_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Check if SPA page is set as WordPress frontpage
|
||||
$frontpage_id = (int) get_option('page_on_front');
|
||||
if (!$frontpage_id || $frontpage_id !== (int) $spa_page_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Get the current request path
|
||||
$request_uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
$path = parse_url($request_uri, PHP_URL_PATH);
|
||||
$path = '/' . trim($path, '/');
|
||||
|
||||
|
||||
// Define SPA routes
|
||||
$spa_routes = ['/', '/shop', '/cart', '/checkout', '/my-account', '/login', '/register', '/reset-password'];
|
||||
|
||||
|
||||
// Check exact matches
|
||||
if (in_array($path, $spa_routes)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check path prefixes
|
||||
$prefix_routes = ['/shop/', '/my-account/', '/product/'];
|
||||
foreach ($prefix_routes as $prefix) {
|
||||
@@ -436,30 +451,31 @@ class Assets {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dequeue conflicting scripts when SPA is active
|
||||
*/
|
||||
public static function dequeue_conflicting_scripts() {
|
||||
public static function dequeue_conflicting_scripts()
|
||||
{
|
||||
if (!self::should_load_assets()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Dequeue WooCommerce scripts that conflict with SPA
|
||||
wp_dequeue_script('wc-cart-fragments');
|
||||
wp_dequeue_script('woocommerce');
|
||||
wp_dequeue_script('wc-add-to-cart');
|
||||
wp_dequeue_script('wc-add-to-cart-variation');
|
||||
|
||||
|
||||
// Dequeue WordPress block scripts that cause errors in SPA
|
||||
wp_dequeue_script('wp-block-library');
|
||||
wp_dequeue_script('wp-block-navigation');
|
||||
wp_dequeue_script('wp-interactivity');
|
||||
wp_dequeue_script('wp-interactivity-router');
|
||||
|
||||
|
||||
// Keep only essential WooCommerce styles, dequeue others if needed
|
||||
// wp_dequeue_style('woocommerce-general');
|
||||
// wp_dequeue_style('woocommerce-layout');
|
||||
|
||||
@@ -781,4 +781,21 @@ class LicenseManager
|
||||
$license_id
|
||||
), ARRAY_A);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get licenses by order ID
|
||||
*
|
||||
* @param int $order_id
|
||||
* @return array
|
||||
*/
|
||||
public static function get_licenses_by_order($order_id)
|
||||
{
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
return $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM $table WHERE order_id = %d ORDER BY created_at ASC",
|
||||
$order_id
|
||||
), ARRAY_A);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ class TemplateRegistry
|
||||
*/
|
||||
public static function get_templates()
|
||||
{
|
||||
return [
|
||||
return apply_filters('woonoow_page_templates', [
|
||||
[
|
||||
'id' => 'blank',
|
||||
'label' => 'Blank Page',
|
||||
@@ -40,7 +40,7 @@ class TemplateRegistry
|
||||
'icon' => 'mail',
|
||||
'sections' => self::get_contact_structure()
|
||||
]
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user