From d7505252ac20953f73d37b4fec13f0d67358f8db Mon Sep 17 00:00:00 2001 From: Dwindi Ramadhana Date: Wed, 31 Dec 2025 21:17:59 +0700 Subject: [PATCH] feat: complete Newsletter Campaigns Phase 1 - Add default campaign email template to DefaultTemplates.php - Add toggle settings (campaign_scheduling, subscriber_limit_enabled) - Add public unsubscribe endpoint with secure token verification - Update CampaignManager to use NewsletterController unsubscribe URLs - Add generate_unsubscribe_url() helper for email templates --- includes/Api/NewsletterController.php | 91 +++++++++++++++++++++ includes/Core/Campaigns/CampaignManager.php | 8 +- includes/Email/DefaultTemplates.php | 26 ++++++ includes/Modules/NewsletterSettings.php | 19 +++++ 4 files changed, 138 insertions(+), 6 deletions(-) diff --git a/includes/Api/NewsletterController.php b/includes/Api/NewsletterController.php index 70d408b..15ba841 100644 --- a/includes/Api/NewsletterController.php +++ b/includes/Api/NewsletterController.php @@ -56,6 +56,23 @@ class NewsletterController { return current_user_can('manage_options'); }, ]); + + // Public unsubscribe endpoint (no auth needed, uses token) + register_rest_route(self::API_NAMESPACE, '/newsletter/unsubscribe', [ + 'methods' => 'GET', + 'callback' => [__CLASS__, 'unsubscribe'], + 'permission_callback' => '__return_true', + 'args' => [ + 'email' => [ + 'required' => true, + 'type' => 'string', + ], + 'token' => [ + 'required' => true, + 'type' => 'string', + ], + ], + ]); } public static function get_template(WP_REST_Request $request) { @@ -197,4 +214,78 @@ class NewsletterController { ], ], 200); } + + /** + * Handle unsubscribe 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)) { + return new WP_REST_Response([ + 'success' => false, + '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'); + $found = true; + break; + } + } + + 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( + '%s

✓ Unsubscribed

You have been unsubscribed from %s newsletter.

', + __('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) { + $secret = wp_salt('auth'); + return hash_hmac('sha256', $email, $secret); + } + + /** + * Generate unsubscribe URL for email templates + */ + 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([ + 'email' => urlencode($email), + 'token' => $token, + ], $base_url); + } } + diff --git a/includes/Core/Campaigns/CampaignManager.php b/includes/Core/Campaigns/CampaignManager.php index 85d8b30..e64369e 100644 --- a/includes/Core/Campaigns/CampaignManager.php +++ b/includes/Core/Campaigns/CampaignManager.php @@ -428,12 +428,8 @@ class CampaignManager { * @return string */ private static function get_unsubscribe_url($email) { - $token = wp_hash($email . 'woonoow_unsubscribe'); - return add_query_arg([ - 'woonoow_unsubscribe' => 1, - 'email' => urlencode($email), - 'token' => $token, - ], home_url()); + // Use NewsletterController's secure token-based URL + return \WooNooW\API\NewsletterController::generate_unsubscribe_url($email); } /** diff --git a/includes/Email/DefaultTemplates.php b/includes/Email/DefaultTemplates.php index 27466c1..1a6fd20 100644 --- a/includes/Email/DefaultTemplates.php +++ b/includes/Email/DefaultTemplates.php @@ -90,6 +90,7 @@ class DefaultTemplates 'order_cancelled' => self::customer_order_cancelled(), 'order_refunded' => self::customer_order_refunded(), 'new_customer' => self::customer_new_customer(), + 'newsletter_campaign' => self::customer_newsletter_campaign(), ], 'staff' => [ 'order_placed' => self::staff_order_placed(), @@ -139,6 +140,7 @@ class DefaultTemplates 'order_cancelled' => 'Order #{order_number} has been cancelled', 'order_refunded' => 'Refund processed for order #{order_number}', 'new_customer' => 'Welcome to {site_name}! 🎁 Exclusive offer inside', + 'newsletter_campaign' => '{campaign_title}', ], 'staff' => [ 'order_placed' => '[NEW ORDER] #{order_number} - ${order_total} from {customer_name}', @@ -206,6 +208,30 @@ Got questions? Our customer service team is ready to help: {support_email} [/card]'; } + /** + * Customer: Newsletter Campaign + * Master design template for newsletter campaigns + * The {content} variable is replaced with the actual campaign content + */ + private static function customer_newsletter_campaign() + { + return '[card type="hero"] +## {campaign_title} +[/card] + +[card] +{content} +[/card] + +[card type="basic" bg="#f5f5f5"] +You are receiving this because you subscribed to {site_name} newsletter. + +[Unsubscribe]({unsubscribe_url}) | [Visit Store]({site_url}) + +© {current_year} {site_name}. All rights reserved. +[/card]'; + } + /** * Customer: Order Placed * Sent immediately when customer places an order diff --git a/includes/Modules/NewsletterSettings.php b/includes/Modules/NewsletterSettings.php index 7dc601e..aa48587 100644 --- a/includes/Modules/NewsletterSettings.php +++ b/includes/Modules/NewsletterSettings.php @@ -75,6 +75,25 @@ class NewsletterSettings { 'placeholder' => __('I agree to receive marketing emails', 'woonoow'), 'default' => __('I agree to receive marketing emails and understand I can unsubscribe at any time.', 'woonoow'), ], + // Campaign Settings + 'campaign_scheduling' => [ + 'type' => 'toggle', + 'label' => __('Campaign Scheduling', 'woonoow'), + 'description' => __('Enable scheduled campaigns. When on, you can schedule campaigns to be sent at a specific date and time.', 'woonoow'), + 'default' => false, + ], + 'subscriber_limit_enabled' => [ + 'type' => 'toggle', + 'label' => __('Subscriber Limit', 'woonoow'), + 'description' => __('Limit subscribers to 1000. When disabled, a custom database table will be created for unlimited subscribers.', 'woonoow'), + 'default' => true, + ], + 'subscriber_limit' => [ + 'type' => 'number', + 'label' => __('Max Subscribers', 'woonoow'), + 'description' => __('Maximum number of subscribers when limit is enabled (default: 1000)', 'woonoow'), + 'default' => 1000, + ], ]; return $schemas;