From 0b2c8a56d66d5e1352065faa154597c2dc93ddb6 Mon Sep 17 00:00:00 2001 From: Dwindi Ramadhana Date: Fri, 26 Dec 2025 10:59:48 +0700 Subject: [PATCH] feat: Newsletter system improvements and validation framework - Fix: Marketing events now display in Staff notifications tab - Reorganize: Move Coupons to Marketing/Coupons for better organization - Add: Comprehensive email/phone validation with extensible filter hooks - Email validation with regex pattern (xxxx@xxxx.xx) - Phone validation with WhatsApp verification support - Filter hooks for external API integration (QuickEmailVerification, etc.) - Fix: Newsletter template routes now use centralized notification email builder - Add: Validation.php class for reusable validation logic - Add: VALIDATION_HOOKS.md documentation with integration examples - Add: NEWSLETTER_CAMPAIGN_PLAN.md architecture for future campaign system - Fix: API delete method call in Newsletter.tsx (delete -> del) - Remove: Duplicate EmailTemplates.tsx (using notification system instead) - Update: Newsletter controller to use centralized Validation class Breaking changes: - Coupons routes moved from /routes/Coupons to /routes/Marketing/Coupons - Legacy /coupons routes maintained for backward compatibility --- NEWSLETTER_CAMPAIGN_PLAN.md | 470 ++++++++++++++++++ VALIDATION_HOOKS.md | 293 +++++++++++ admin-spa/src/App.tsx | 13 +- .../{ => Marketing}/Coupons/CouponForm.tsx | 0 .../routes/{ => Marketing}/Coupons/Edit.tsx | 0 .../routes/{ => Marketing}/Coupons/New.tsx | 0 .../Coupons/components/CouponCard.tsx | 0 .../Coupons/components/CouponFilterSheet.tsx | 0 .../routes/{ => Marketing}/Coupons/index.tsx | 0 .../src/routes/Marketing/EmailTemplates.tsx | 126 ----- admin-spa/src/routes/Marketing/Newsletter.tsx | 12 +- .../Settings/Notifications/Staff/Events.tsx | 72 +++ .../src/components/Layout/PageLayout.tsx | 0 customer-spa/src/hooks/useWishlist.ts | 5 +- customer-spa/src/layouts/BaseLayout.tsx | 10 +- customer-spa/src/pages/Account/Addresses.tsx | 29 +- customer-spa/src/pages/Product/index.tsx | 40 +- customer-spa/src/types/product.ts | 6 + includes/Api/NewsletterController.php | 8 +- includes/Api/ProductsController.php | 24 +- includes/Api/StoreController.php | 3 - includes/Compat/CustomerSettingsProvider.php | 35 +- includes/Core/Validation.php | 218 ++++++++ 23 files changed, 1132 insertions(+), 232 deletions(-) create mode 100644 NEWSLETTER_CAMPAIGN_PLAN.md create mode 100644 VALIDATION_HOOKS.md rename admin-spa/src/routes/{ => Marketing}/Coupons/CouponForm.tsx (100%) rename admin-spa/src/routes/{ => Marketing}/Coupons/Edit.tsx (100%) rename admin-spa/src/routes/{ => Marketing}/Coupons/New.tsx (100%) rename admin-spa/src/routes/{ => Marketing}/Coupons/components/CouponCard.tsx (100%) rename admin-spa/src/routes/{ => Marketing}/Coupons/components/CouponFilterSheet.tsx (100%) rename admin-spa/src/routes/{ => Marketing}/Coupons/index.tsx (100%) delete mode 100644 admin-spa/src/routes/Marketing/EmailTemplates.tsx create mode 100644 customer-spa/src/components/Layout/PageLayout.tsx create mode 100644 includes/Core/Validation.php diff --git a/NEWSLETTER_CAMPAIGN_PLAN.md b/NEWSLETTER_CAMPAIGN_PLAN.md new file mode 100644 index 0000000..b3550bd --- /dev/null +++ b/NEWSLETTER_CAMPAIGN_PLAN.md @@ -0,0 +1,470 @@ +# Newsletter Campaign System - Architecture Plan + +## Overview + +A comprehensive newsletter system that separates **design templates** from **campaign content**, allowing efficient email broadcasting to subscribers without rebuilding existing infrastructure. + +--- + +## System Architecture + +### 1. **Subscriber Management** ✅ (Already Built) +- **Location**: `Marketing > Newsletter > Subscribers List` +- **Features**: + - Email collection with validation (format + optional external API) + - Subscriber metadata (email, user_id, status, subscribed_at, ip_address) + - Search/filter subscribers + - Export to CSV + - Delete subscribers +- **Storage**: WordPress options table (`woonoow_newsletter_subscribers`) + +### 2. **Email Design Templates** ✅ (Already Built - Reuse Notification System) +- **Location**: Settings > Notifications > Email Builder +- **Purpose**: Create the **visual design/layout** for newsletters +- **Features**: + - Visual block editor (drag-and-drop cards, buttons, text) + - Markdown editor (mobile-friendly) + - Live preview with branding (logo, colors, social links) + - Shortcode support: `{campaign_title}`, `{campaign_content}`, `{unsubscribe_url}`, `{subscriber_email}`, `{site_name}`, etc. +- **Storage**: Same as notification templates (`wp_options` or custom table) +- **Events to Create**: + - `newsletter_campaign` (customer, marketing category) - For broadcast emails + +**Template Structure Example**: +```markdown +[card:hero] +# {campaign_title} +[/card] + +[card] +{campaign_content} +[/card] + +[card:basic] +--- +You're receiving this because you subscribed to our newsletter. +[Unsubscribe]({unsubscribe_url}) +[/card] +``` + +### 3. **Campaign Management** 🆕 (New Module) +- **Location**: `Marketing > Newsletter > Campaigns` (new tab) +- **Purpose**: Create campaign **content/message** that uses design templates +- **Features**: + - Campaign list (draft, scheduled, sent, failed) + - Create/edit campaign + - Select design template + - Write campaign content (rich text editor - text only, no design) + - Preview (merge template + content) + - Schedule or send immediately + - Target audience (all subscribers, filtered by date, user_id, etc.) + - Track status (pending, sending, sent, failed) + +--- + +## Database Schema + +### Table: `wp_woonoow_campaigns` + +```sql +CREATE TABLE wp_woonoow_campaigns ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + subject VARCHAR(255) NOT NULL, + content LONGTEXT NOT NULL, + template_id VARCHAR(100) DEFAULT 'newsletter_campaign', + status ENUM('draft', 'scheduled', 'sending', 'sent', 'failed') DEFAULT 'draft', + scheduled_at DATETIME NULL, + sent_at DATETIME NULL, + total_recipients INT DEFAULT 0, + sent_count INT DEFAULT 0, + failed_count INT DEFAULT 0, + created_by BIGINT UNSIGNED, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_status (status), + INDEX idx_scheduled (scheduled_at), + INDEX idx_created_by (created_by) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### Table: `wp_woonoow_campaign_logs` + +```sql +CREATE TABLE wp_woonoow_campaign_logs ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + campaign_id BIGINT UNSIGNED NOT NULL, + subscriber_email VARCHAR(255) NOT NULL, + status ENUM('pending', 'sent', 'failed') DEFAULT 'pending', + error_message TEXT NULL, + sent_at DATETIME NULL, + INDEX idx_campaign (campaign_id), + INDEX idx_status (status), + FOREIGN KEY (campaign_id) REFERENCES wp_woonoow_campaigns(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +--- + +## API Endpoints + +### Campaign CRUD + +```php +// GET /woonoow/v1/newsletter/campaigns +// List all campaigns with pagination +CampaignsController::list_campaigns() + +// GET /woonoow/v1/newsletter/campaigns/{id} +// Get single campaign +CampaignsController::get_campaign($id) + +// POST /woonoow/v1/newsletter/campaigns +// Create new campaign +CampaignsController::create_campaign($data) + +// PUT /woonoow/v1/newsletter/campaigns/{id} +// Update campaign +CampaignsController::update_campaign($id, $data) + +// DELETE /woonoow/v1/newsletter/campaigns/{id} +// Delete campaign +CampaignsController::delete_campaign($id) + +// POST /woonoow/v1/newsletter/campaigns/{id}/preview +// Preview campaign (merge template + content) +CampaignsController::preview_campaign($id) + +// POST /woonoow/v1/newsletter/campaigns/{id}/send +// Send campaign immediately or schedule +CampaignsController::send_campaign($id, $schedule_time) + +// GET /woonoow/v1/newsletter/campaigns/{id}/stats +// Get campaign statistics +CampaignsController::get_campaign_stats($id) + +// GET /woonoow/v1/newsletter/templates +// List available design templates +CampaignsController::list_templates() +``` + +--- + +## UI Components + +### 1. Campaign List Page +**Route**: `/marketing/newsletter?tab=campaigns` + +**Features**: +- Table with columns: Title, Subject, Status, Recipients, Sent Date, Actions +- Filter by status (draft, scheduled, sent, failed) +- Search by title/subject +- Actions: Edit, Preview, Duplicate, Delete, Send Now +- "Create Campaign" button + +### 2. Campaign Editor +**Route**: `/marketing/newsletter/campaigns/new` or `/marketing/newsletter/campaigns/{id}/edit` + +**Form Fields**: +```tsx +- Campaign Title (internal name) +- Email Subject (what subscribers see) +- Design Template (dropdown: select from available templates) +- Campaign Content (rich text editor - TipTap or similar) + - Bold, italic, links, headings, lists + - NO design elements (cards, buttons) - those are in template +- Preview Button (opens modal with merged template + content) +- Target Audience (future: filters, for now: all subscribers) +- Schedule Options: + - Send Now + - Schedule for Later (date/time picker) +- Save as Draft +``` + +### 3. Preview Modal +**Component**: `CampaignPreview.tsx` + +**Features**: +- Fetch design template +- Replace `{campaign_title}` with campaign title +- Replace `{campaign_content}` with campaign content +- Replace `{unsubscribe_url}` with sample URL +- Show full email preview with branding +- "Send Test Email" button (send to admin email) + +### 4. Campaign Stats Page +**Route**: `/marketing/newsletter/campaigns/{id}/stats` + +**Metrics**: +- Total recipients +- Sent count +- Failed count +- Sent date/time +- Error log (for failed emails) + +--- + +## Sending System + +### WP-Cron Job +```php +// Schedule hourly check for pending campaigns +add_action('woonoow_send_scheduled_campaigns', 'WooNooW\Core\CampaignSender::process_scheduled'); + +// Register cron schedule +if (!wp_next_scheduled('woonoow_send_scheduled_campaigns')) { + wp_schedule_event(time(), 'hourly', 'woonoow_send_scheduled_campaigns'); +} +``` + +### Batch Processing +```php +class CampaignSender { + const BATCH_SIZE = 50; // Send 50 emails per batch + const BATCH_DELAY = 5; // 5 seconds between batches + + public static function process_scheduled() { + // Find campaigns where status='scheduled' and scheduled_at <= now + $campaigns = self::get_pending_campaigns(); + + foreach ($campaigns as $campaign) { + self::send_campaign($campaign->id); + } + } + + public static function send_campaign($campaign_id) { + $campaign = self::get_campaign($campaign_id); + $subscribers = self::get_subscribers(); + + // Update status to 'sending' + self::update_campaign_status($campaign_id, 'sending'); + + // Get design template + $template = self::get_template($campaign->template_id); + + // Process in batches + $batches = array_chunk($subscribers, self::BATCH_SIZE); + + foreach ($batches as $batch) { + foreach ($batch as $subscriber) { + self::send_to_subscriber($campaign, $template, $subscriber); + } + + // Delay between batches to avoid rate limits + sleep(self::BATCH_DELAY); + } + + // Update status to 'sent' + self::update_campaign_status($campaign_id, 'sent', [ + 'sent_at' => current_time('mysql'), + 'sent_count' => count($subscribers), + ]); + } + + private static function send_to_subscriber($campaign, $template, $subscriber) { + // Merge template with campaign content + $email_body = self::merge_template($template, $campaign, $subscriber); + + // Send via notification system + do_action('woonoow/notification/send', [ + 'event' => 'newsletter_campaign', + 'channel' => 'email', + 'recipient' => $subscriber['email'], + 'subject' => $campaign->subject, + 'body' => $email_body, + 'data' => [ + 'campaign_id' => $campaign->id, + 'subscriber_email' => $subscriber['email'], + ], + ]); + + // Log send attempt + self::log_send($campaign->id, $subscriber['email'], 'sent'); + } + + private static function merge_template($template, $campaign, $subscriber) { + $body = $template->body; + + // Replace campaign variables + $body = str_replace('{campaign_title}', $campaign->title, $body); + $body = str_replace('{campaign_content}', $campaign->content, $body); + + // Replace subscriber variables + $body = str_replace('{subscriber_email}', $subscriber['email'], $body); + $unsubscribe_url = add_query_arg([ + 'action' => 'woonoow_unsubscribe', + 'email' => base64_encode($subscriber['email']), + 'token' => wp_create_nonce('unsubscribe_' . $subscriber['email']), + ], home_url()); + $body = str_replace('{unsubscribe_url}', $unsubscribe_url, $body); + + // Replace site variables + $body = str_replace('{site_name}', get_bloginfo('name'), $body); + + return $body; + } +} +``` + +--- + +## Workflow + +### Creating a Campaign + +1. **Admin goes to**: Marketing > Newsletter > Campaigns +2. **Clicks**: "Create Campaign" +3. **Fills form**: + - Title: "Summer Sale 2025" + - Subject: "🌞 50% Off Summer Collection!" + - Template: Select "Newsletter Campaign" (design template) + - Content: Write message in rich text editor + ``` + Hi there! + + We're excited to announce our biggest summer sale yet! + + Get 50% off all summer items this week only. + + Shop now and save big! + ``` +4. **Clicks**: "Preview" → See full email with design + content merged +5. **Clicks**: "Send Test Email" → Receive test at admin email +6. **Chooses**: "Schedule for Later" → Select date/time +7. **Clicks**: "Save & Schedule" + +### Sending Process + +1. **WP-Cron runs** every hour +2. **Finds** campaigns where `status='scheduled'` and `scheduled_at <= now` +3. **Processes** each campaign: + - Updates status to `sending` + - Gets all subscribers + - Sends in batches of 50 + - Logs each send attempt + - Updates status to `sent` when complete +4. **Admin can view** stats: total sent, failed, errors + +--- + +## Minimal Feature Set (MVP) + +### Phase 1: Core Campaign System +- ✅ Database tables (campaigns, campaign_logs) +- ✅ API endpoints (CRUD, preview, send) +- ✅ Campaign list UI +- ✅ Campaign editor UI +- ✅ Preview modal +- ✅ Send immediately functionality +- ✅ Basic stats page + +### Phase 2: Scheduling & Automation +- ✅ Schedule for later +- ✅ WP-Cron integration +- ✅ Batch processing +- ✅ Error handling & logging + +### Phase 3: Enhancements (Future) +- 📧 Open tracking (pixel) +- 🔗 Click tracking (link wrapping) +- 🎯 Audience segmentation (filter by date, user role, etc.) +- 📊 Analytics dashboard +- 📋 Campaign templates library +- 🔄 A/B testing +- 🤖 Automation workflows + +--- + +## Design Template Variables + +Templates can use these variables (replaced during send): + +### Campaign Variables +- `{campaign_title}` - Campaign title +- `{campaign_content}` - Campaign content (rich text) + +### Subscriber Variables +- `{subscriber_email}` - Subscriber's email +- `{unsubscribe_url}` - Unsubscribe link + +### Site Variables +- `{site_name}` - Site name +- `{site_url}` - Site URL +- `{current_year}` - Current year + +--- + +## File Structure + +``` +includes/ +├── Api/ +│ ├── NewsletterController.php (existing - subscribers) +│ └── CampaignsController.php (new - campaigns CRUD) +├── Core/ +│ ├── Validation.php (existing - email/phone validation) +│ ├── CampaignSender.php (new - sending logic) +│ └── Notifications/ +│ └── EventRegistry.php (add newsletter_campaign event) + +admin-spa/src/routes/Marketing/ +├── Newsletter.tsx (existing - subscribers list) +├── Newsletter/ +│ ├── Campaigns.tsx (new - campaign list) +│ ├── CampaignEditor.tsx (new - create/edit) +│ ├── CampaignPreview.tsx (new - preview modal) +│ └── CampaignStats.tsx (new - stats page) +``` + +--- + +## Key Principles + +1. **Separation of Concerns**: + - Design templates = Visual layout (cards, buttons, colors) + - Campaign content = Message text (what to say) + +2. **Reuse Existing Infrastructure**: + - Email builder (notification system) + - Email sending (notification system) + - Branding settings (email customization) + - Subscriber management (already built) + +3. **Minimal Duplication**: + - Don't rebuild email builder + - Don't rebuild email sending + - Don't rebuild subscriber management + +4. **Efficient Workflow**: + - Create design template once + - Reuse for multiple campaigns + - Only write campaign content each time + +5. **Scalability**: + - Batch processing for large lists + - Queue system for reliability + - Error logging for debugging + +--- + +## Success Metrics + +- ✅ Admin can create campaign in < 2 minutes +- ✅ Preview shows accurate email with branding +- ✅ Emails sent without rate limit issues +- ✅ Failed sends are logged and visible +- ✅ No duplicate code or functionality +- ✅ System handles 10,000+ subscribers efficiently + +--- + +## Next Steps + +1. Create database migration for campaign tables +2. Build `CampaignsController.php` with all API endpoints +3. Create `CampaignSender.php` with batch processing logic +4. Add `newsletter_campaign` event to EventRegistry +5. Build Campaign UI components (list, editor, preview, stats) +6. Test with small subscriber list +7. Optimize batch size and delays +8. Document for users diff --git a/VALIDATION_HOOKS.md b/VALIDATION_HOOKS.md new file mode 100644 index 0000000..5b9dade --- /dev/null +++ b/VALIDATION_HOOKS.md @@ -0,0 +1,293 @@ +# Validation Filter Hooks + +WooNooW provides extensible validation filter hooks that allow addons to integrate external validation services for emails and phone numbers. + +## Email Validation + +### Filter: `woonoow/validate_email` + +Validates email addresses with support for external API integration. + +**Parameters:** +- `$is_valid` (bool|WP_Error): Initial validation state (default: true) +- `$email` (string): The email address to validate +- `$context` (string): Context of validation (e.g., 'newsletter_subscribe', 'checkout', 'registration') + +**Returns:** `true` if valid, `WP_Error` if invalid + +**Built-in Validation:** +1. WordPress `is_email()` check +2. Regex pattern validation: `xxxx@xxxx.xx` format +3. Extensible via filter hook + +### Example: QuickEmailVerification.com Integration + +```php +add_filter('woonoow/validate_email', function($is_valid, $email, $context) { + // Only validate for newsletter subscriptions + if ($context !== 'newsletter_subscribe') { + return $is_valid; + } + + $api_key = get_option('my_addon_quickemail_api_key'); + if (!$api_key) { + return $is_valid; // Skip if no API key configured + } + + // Call QuickEmailVerification API + $response = wp_remote_get( + "https://api.quickemailverification.com/v1/verify?email={$email}&apikey={$api_key}", + ['timeout' => 5] + ); + + if (is_wp_error($response)) { + // Fallback to basic validation on API error + return $is_valid; + } + + $data = json_decode(wp_remote_retrieve_body($response), true); + + // Check validation result + if (isset($data['result']) && $data['result'] !== 'valid') { + return new WP_Error( + 'email_verification_failed', + sprintf('Email verification failed: %s', $data['reason'] ?? 'Unknown'), + ['status' => 400] + ); + } + + return true; +}, 10, 3); +``` + +### Example: Hunter.io Email Verification + +```php +add_filter('woonoow/validate_email', function($is_valid, $email, $context) { + $api_key = get_option('my_addon_hunter_api_key'); + if (!$api_key) return $is_valid; + + $response = wp_remote_get( + "https://api.hunter.io/v2/email-verifier?email={$email}&api_key={$api_key}" + ); + + if (is_wp_error($response)) return $is_valid; + + $data = json_decode(wp_remote_retrieve_body($response), true); + + if ($data['data']['status'] !== 'valid') { + return new WP_Error('email_invalid', 'Email address is not deliverable'); + } + + return true; +}, 10, 3); +``` + +--- + +## Phone Validation + +### Filter: `woonoow/validate_phone` + +Validates phone numbers with support for external API integration and WhatsApp verification. + +**Parameters:** +- `$is_valid` (bool|WP_Error): Initial validation state (default: true) +- `$phone` (string): The phone number to validate (cleaned, no formatting) +- `$context` (string): Context of validation (e.g., 'checkout', 'registration', 'shipping') +- `$country_code` (string): Country code if available (e.g., 'ID', 'US') + +**Returns:** `true` if valid, `WP_Error` if invalid + +**Built-in Validation:** +1. Format check: 8-15 digits, optional `+` prefix +2. Removes common formatting characters +3. Extensible via filter hook + +### Example: WhatsApp Number Verification + +```php +add_filter('woonoow/validate_phone', function($is_valid, $phone, $context, $country_code) { + // Only validate for checkout + if ($context !== 'checkout') { + return $is_valid; + } + + $api_token = get_option('my_addon_whatsapp_api_token'); + if (!$api_token) return $is_valid; + + // Check if number is registered on WhatsApp + $response = wp_remote_post('https://api.whatsapp.com/v1/contacts', [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $api_token, + 'Content-Type' => 'application/json', + ], + 'body' => json_encode([ + 'blocking' => 'wait', + 'contacts' => [$phone], + ]), + 'timeout' => 10, + ]); + + if (is_wp_error($response)) { + return $is_valid; // Fallback on API error + } + + $data = json_decode(wp_remote_retrieve_body($response), true); + + // Check if WhatsApp ID exists + if (!isset($data['contacts'][0]['wa_id'])) { + return new WP_Error( + 'phone_not_whatsapp', + 'Phone number must be registered on WhatsApp for order notifications', + ['status' => 400] + ); + } + + return true; +}, 10, 4); +``` + +### Example: Numverify Phone Validation + +```php +add_filter('woonoow/validate_phone', function($is_valid, $phone, $context, $country_code) { + $api_key = get_option('my_addon_numverify_api_key'); + if (!$api_key) return $is_valid; + + $url = sprintf( + 'http://apilayer.net/api/validate?access_key=%s&number=%s&country_code=%s', + $api_key, + urlencode($phone), + urlencode($country_code) + ); + + $response = wp_remote_get($url, ['timeout' => 5]); + + if (is_wp_error($response)) return $is_valid; + + $data = json_decode(wp_remote_retrieve_body($response), true); + + if (!$data['valid']) { + return new WP_Error( + 'phone_invalid', + sprintf('Invalid phone number: %s', $data['error'] ?? 'Unknown error') + ); + } + + // Store carrier info for later use + update_post_meta(get_current_user_id(), '_phone_carrier', $data['carrier'] ?? ''); + + return true; +}, 10, 4); +``` + +### Filter: `woonoow/validate_phone_whatsapp` + +Convenience filter specifically for WhatsApp registration checks. + +**Parameters:** +- `$is_registered` (bool|WP_Error): Initial state (default: true) +- `$phone` (string): The phone number (cleaned) +- `$context` (string): Context of validation +- `$country_code` (string): Country code if available + +**Returns:** `true` if registered on WhatsApp, `WP_Error` if not + +--- + +## Usage in Code + +### Email Validation + +```php +use WooNooW\Core\Validation; + +// Validate email for newsletter +$result = Validation::validate_email('user@example.com', 'newsletter_subscribe'); + +if (is_wp_error($result)) { + // Handle error + echo $result->get_error_message(); +} else { + // Email is valid + // Proceed with subscription +} +``` + +### Phone Validation + +```php +use WooNooW\Core\Validation; + +// Validate phone for checkout +$result = Validation::validate_phone('+628123456789', 'checkout', 'ID'); + +if (is_wp_error($result)) { + // Handle error + echo $result->get_error_message(); +} else { + // Phone is valid + // Proceed with order +} +``` + +### Phone + WhatsApp Validation + +```php +use WooNooW\Core\Validation; + +// Validate phone and check WhatsApp registration +$result = Validation::validate_phone_whatsapp('+628123456789', 'checkout', 'ID'); + +if (is_wp_error($result)) { + // Phone invalid or not registered on WhatsApp + echo $result->get_error_message(); +} else { + // Phone is valid and registered on WhatsApp + // Proceed with order +} +``` + +--- + +## Validation Contexts + +Common contexts used throughout WooNooW: + +- `newsletter_subscribe` - Newsletter subscription form +- `checkout` - Checkout process +- `registration` - User registration +- `shipping` - Shipping address validation +- `billing` - Billing address validation +- `general` - General validation (default) + +Addons can filter based on context to apply different validation rules for different scenarios. + +--- + +## Best Practices + +1. **Always fallback gracefully** - If external API fails, return `$is_valid` to use basic validation +2. **Use timeouts** - Set reasonable timeouts (5-10 seconds) for API calls +3. **Cache results** - Cache validation results to avoid repeated API calls +4. **Provide clear error messages** - Return descriptive WP_Error messages +5. **Check context** - Only apply validation where needed to avoid unnecessary API calls +6. **Handle API keys securely** - Store API keys in options, never hardcode +7. **Log errors** - Log API errors for debugging without blocking users + +--- + +## Error Codes + +### Email Validation Errors +- `invalid_email` - Basic format validation failed +- `invalid_email_format` - Regex pattern validation failed +- `email_verification_failed` - External API verification failed +- `email_validation_failed` - Generic validation failure + +### Phone Validation Errors +- `invalid_phone` - Basic format validation failed +- `phone_not_whatsapp` - Phone not registered on WhatsApp +- `phone_invalid` - External API validation failed +- `phone_validation_failed` - Generic validation failure diff --git a/admin-spa/src/App.tsx b/admin-spa/src/App.tsx index b3302b8..5d420a6 100644 --- a/admin-spa/src/App.tsx +++ b/admin-spa/src/App.tsx @@ -18,9 +18,9 @@ import ProductEdit from '@/routes/Products/Edit'; import ProductCategories from '@/routes/Products/Categories'; import ProductTags from '@/routes/Products/Tags'; import ProductAttributes from '@/routes/Products/Attributes'; -import CouponsIndex from '@/routes/Coupons'; -import CouponNew from '@/routes/Coupons/New'; -import CouponEdit from '@/routes/Coupons/Edit'; +import CouponsIndex from '@/routes/Marketing/Coupons'; +import CouponNew from '@/routes/Marketing/Coupons/New'; +import CouponEdit from '@/routes/Marketing/Coupons/Edit'; import CustomersIndex from '@/routes/Customers'; import CustomerNew from '@/routes/Customers/New'; import CustomerEdit from '@/routes/Customers/Edit'; @@ -250,7 +250,6 @@ import AppearanceThankYou from '@/routes/Appearance/ThankYou'; import AppearanceAccount from '@/routes/Appearance/Account'; import MarketingIndex from '@/routes/Marketing'; import NewsletterSubscribers from '@/routes/Marketing/Newsletter'; -import EmailTemplates from '@/routes/Marketing/EmailTemplates'; import MorePage from '@/routes/More'; // Addon Route Component - Dynamically loads addon components @@ -515,10 +514,13 @@ function AppRoutes() { } /> } /> - {/* Coupons */} + {/* Coupons (under Marketing) */} } /> } /> } /> + } /> + } /> + } /> {/* Customers */} } /> @@ -565,7 +567,6 @@ function AppRoutes() { {/* Marketing */} } /> } /> - } /> {/* Dynamic Addon Routes */} {addonRoutes.map((route: any) => ( diff --git a/admin-spa/src/routes/Coupons/CouponForm.tsx b/admin-spa/src/routes/Marketing/Coupons/CouponForm.tsx similarity index 100% rename from admin-spa/src/routes/Coupons/CouponForm.tsx rename to admin-spa/src/routes/Marketing/Coupons/CouponForm.tsx diff --git a/admin-spa/src/routes/Coupons/Edit.tsx b/admin-spa/src/routes/Marketing/Coupons/Edit.tsx similarity index 100% rename from admin-spa/src/routes/Coupons/Edit.tsx rename to admin-spa/src/routes/Marketing/Coupons/Edit.tsx diff --git a/admin-spa/src/routes/Coupons/New.tsx b/admin-spa/src/routes/Marketing/Coupons/New.tsx similarity index 100% rename from admin-spa/src/routes/Coupons/New.tsx rename to admin-spa/src/routes/Marketing/Coupons/New.tsx diff --git a/admin-spa/src/routes/Coupons/components/CouponCard.tsx b/admin-spa/src/routes/Marketing/Coupons/components/CouponCard.tsx similarity index 100% rename from admin-spa/src/routes/Coupons/components/CouponCard.tsx rename to admin-spa/src/routes/Marketing/Coupons/components/CouponCard.tsx diff --git a/admin-spa/src/routes/Coupons/components/CouponFilterSheet.tsx b/admin-spa/src/routes/Marketing/Coupons/components/CouponFilterSheet.tsx similarity index 100% rename from admin-spa/src/routes/Coupons/components/CouponFilterSheet.tsx rename to admin-spa/src/routes/Marketing/Coupons/components/CouponFilterSheet.tsx diff --git a/admin-spa/src/routes/Coupons/index.tsx b/admin-spa/src/routes/Marketing/Coupons/index.tsx similarity index 100% rename from admin-spa/src/routes/Coupons/index.tsx rename to admin-spa/src/routes/Marketing/Coupons/index.tsx diff --git a/admin-spa/src/routes/Marketing/EmailTemplates.tsx b/admin-spa/src/routes/Marketing/EmailTemplates.tsx deleted file mode 100644 index add8ceb..0000000 --- a/admin-spa/src/routes/Marketing/EmailTemplates.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React, { useState } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { SettingsLayout } from '@/routes/Settings/components/SettingsLayout'; -import { SettingsCard } from '@/routes/Settings/components/SettingsCard'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Textarea } from '@/components/ui/textarea'; -import { toast } from 'sonner'; -import { api } from '@/lib/api'; -import { ArrowLeft, Save } from 'lucide-react'; -import { useNavigate, useParams } from 'react-router-dom'; - -export default function EmailTemplates() { - const navigate = useNavigate(); - const { template } = useParams(); - const queryClient = useQueryClient(); - - const [subject, setSubject] = useState(''); - const [content, setContent] = useState(''); - - const { data: templateData, isLoading } = useQuery({ - queryKey: ['newsletter-template', template], - queryFn: async () => { - const response = await api.get(`/newsletter/template/${template}`); - return response.data; - }, - enabled: !!template, - }); - - React.useEffect(() => { - if (templateData) { - setSubject(templateData.subject || ''); - setContent(templateData.content || ''); - } - }, [templateData]); - - const saveTemplate = useMutation({ - mutationFn: async () => { - await api.post(`/newsletter/template/${template}`, { - subject, - content, - }); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['newsletter-template'] }); - toast.success('Template saved successfully'); - }, - onError: () => { - toast.error('Failed to save template'); - }, - }); - - const handleSave = () => { - saveTemplate.mutate(); - }; - - return ( - -
- -
- - -
-
- - setSubject(e.target.value)} - placeholder="Welcome to {site_name} Newsletter!" - /> -
- -
- -