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;