fix: template save API + contextual variables per event

1. API Route Fix (NotificationsController.php):
   - Changed PUT to POST for /templates/:eventId/:channelId
   - Frontend was using api.post() but backend only accepted PUT
   - Templates can now be saved

2. Contextual Variables (EventRegistry.php):
   - Added get_variables_for_event() method
   - Returns category-based variables (order, customer, product, etc.)
   - Merges event-specific variables from event definition
   - Sorted alphabetically for easy browsing

3. API Response (NotificationsController.php):
   - Template API now returns available_variables for the event
   - Frontend can show only relevant variables

4. Frontend (EditTemplate.tsx):
   - Removed hardcoded 50+ variable list
   - Now uses template.available_variables from API
   - Variables update based on selected event type
This commit is contained in:
Dwindi Ramadhana
2026-01-01 21:31:10 +07:00
parent b8f179a984
commit ccdd88a629
3 changed files with 265 additions and 162 deletions

View File

@@ -18,7 +18,7 @@ import { markdownToHtml } from '@/lib/markdown-utils';
export default function EditTemplate() { export default function EditTemplate() {
// Mobile responsive check // Mobile responsive check
const [isMobile, setIsMobile] = useState(false); const [isMobile, setIsMobile] = useState(false);
useEffect(() => { useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768); const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile(); checkMobile();
@@ -28,63 +28,15 @@ export default function EditTemplate() {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const eventId = searchParams.get('event'); const eventId = searchParams.get('event');
const channelId = searchParams.get('channel'); const channelId = searchParams.get('channel');
const recipientType = searchParams.get('recipient') || 'customer'; // Default to customer const recipientType = searchParams.get('recipient') || 'customer'; // Default to customer
const [subject, setSubject] = useState(''); const [subject, setSubject] = useState('');
const [markdownContent, setMarkdownContent] = useState(''); // Source of truth: Markdown const [markdownContent, setMarkdownContent] = useState(''); // Source of truth: Markdown
const [blocks, setBlocks] = useState<EmailBlock[]>([]); // Visual mode view (derived from markdown) const [blocks, setBlocks] = useState<EmailBlock[]>([]); // Visual mode view (derived from markdown)
const [activeTab, setActiveTab] = useState('preview'); const [activeTab, setActiveTab] = useState('preview');
// All available template variables
const availableVariables = [
// Order variables
'order_number',
'order_id',
'order_date',
'order_total',
'order_subtotal',
'order_tax',
'order_shipping',
'order_discount',
'order_status',
'order_url',
'order_items_table',
'completion_date',
'estimated_delivery',
// Customer variables
'customer_name',
'customer_first_name',
'customer_last_name',
'customer_email',
'customer_phone',
'billing_address',
'shipping_address',
// Payment variables
'payment_method',
'payment_status',
'payment_date',
'transaction_id',
'payment_retry_url',
// Shipping/Tracking variables
'tracking_number',
'tracking_url',
'shipping_carrier',
'shipping_method',
// URL variables
'review_url',
'shop_url',
'my_account_url',
// Store variables
'site_name',
'site_title',
'store_name',
'store_url',
'support_email',
'current_year',
];
// Fetch email customization settings // Fetch email customization settings
const { data: emailSettings } = useQuery({ const { data: emailSettings } = useQuery({
@@ -101,20 +53,20 @@ export default function EditTemplate() {
console.log('API Response:', response); console.log('API Response:', response);
console.log('API Response.data:', response.data); console.log('API Response.data:', response.data);
console.log('API Response type:', typeof response); console.log('API Response type:', typeof response);
// The api.get might already unwrap response.data // The api.get might already unwrap response.data
// Return the response directly if it has the template fields // Return the response directly if it has the template fields
if (response && (response.subject !== undefined || response.body !== undefined)) { if (response && (response.subject !== undefined || response.body !== undefined)) {
console.log('Returning response directly:', response); console.log('Returning response directly:', response);
return response; return response;
} }
// Otherwise return response.data // Otherwise return response.data
if (response && response.data) { if (response && response.data) {
console.log('Returning response.data:', response.data); console.log('Returning response.data:', response.data);
return response.data; return response.data;
} }
return null; return null;
}, },
enabled: !!eventId && !!channelId, enabled: !!eventId && !!channelId,
@@ -123,11 +75,11 @@ export default function EditTemplate() {
useEffect(() => { useEffect(() => {
if (template) { if (template) {
setSubject(template.subject || ''); setSubject(template.subject || '');
// Always treat body as markdown (source of truth) // Always treat body as markdown (source of truth)
const markdown = template.body || ''; const markdown = template.body || '';
setMarkdownContent(markdown); setMarkdownContent(markdown);
// Convert to blocks for visual mode // Convert to blocks for visual mode
const initialBlocks = markdownToBlocks(markdown); const initialBlocks = markdownToBlocks(markdown);
setBlocks(initialBlocks); setBlocks(initialBlocks);
@@ -151,7 +103,7 @@ export default function EditTemplate() {
const handleReset = async () => { const handleReset = async () => {
if (!confirm(__('Are you sure you want to reset this template to default?'))) return; if (!confirm(__('Are you sure you want to reset this template to default?'))) return;
try { try {
await api.del(`/notifications/templates/${eventId}/${channelId}?recipient=${recipientType}`); await api.del(`/notifications/templates/${eventId}/${channelId}?recipient=${recipientType}`);
queryClient.invalidateQueries({ queryKey: ['notification-templates'] }); queryClient.invalidateQueries({ queryKey: ['notification-templates'] });
@@ -168,7 +120,7 @@ export default function EditTemplate() {
const markdown = blocksToMarkdown(newBlocks); const markdown = blocksToMarkdown(newBlocks);
setMarkdownContent(markdown); // Update markdown (source of truth) setMarkdownContent(markdown); // Update markdown (source of truth)
}; };
// Markdown mode: Update markdown → Blocks (for visual sync) // Markdown mode: Update markdown → Blocks (for visual sync)
const handleMarkdownChange = (newMarkdown: string) => { const handleMarkdownChange = (newMarkdown: string) => {
setMarkdownContent(newMarkdown); // Update source of truth setMarkdownContent(newMarkdown); // Update source of truth
@@ -176,9 +128,11 @@ export default function EditTemplate() {
setBlocks(newBlocks); // Keep blocks in sync setBlocks(newBlocks); // Keep blocks in sync
}; };
// Variable keys for the rich text editor dropdown // Variable keys for the rich text editor dropdown - from API (contextual per event)
const variableKeys = availableVariables; const variableKeys = template?.available_variables
? Object.keys(template.available_variables).map(k => k.replace(/^\{|}$/g, ''))
: [];
// Parse [card] tags and [button] shortcodes for preview // Parse [card] tags and [button] shortcodes for preview
const parseCardsForPreview = (content: string) => { const parseCardsForPreview = (content: string) => {
// Parse card blocks - new [card:type] syntax // Parse card blocks - new [card:type] syntax
@@ -187,7 +141,7 @@ export default function EditTemplate() {
const htmlContent = markdownToHtml(cardContent.trim()); const htmlContent = markdownToHtml(cardContent.trim());
return `<div class="${cardClass}">${htmlContent}</div>`; return `<div class="${cardClass}">${htmlContent}</div>`;
}); });
// Parse card blocks - old [card type="..."] syntax (backward compatibility) // Parse card blocks - old [card type="..."] syntax (backward compatibility)
parsed = parsed.replace(/\[card([^\]]*)\](.*?)\[\/card\]/gs, (match, attributes, cardContent) => { parsed = parsed.replace(/\[card([^\]]*)\](.*?)\[\/card\]/gs, (match, attributes, cardContent) => {
let cardClass = 'card'; let cardClass = 'card';
@@ -195,27 +149,27 @@ export default function EditTemplate() {
if (typeMatch) { if (typeMatch) {
cardClass += ` card-${typeMatch[1]}`; cardClass += ` card-${typeMatch[1]}`;
} }
const bgMatch = attributes.match(/bg=["']([^"']+)["']/); const bgMatch = attributes.match(/bg=["']([^"']+)["']/);
const bgStyle = bgMatch ? `background-image: url(${bgMatch[1]}); background-size: cover; background-position: center;` : ''; const bgStyle = bgMatch ? `background-image: url(${bgMatch[1]}); background-size: cover; background-position: center;` : '';
// Convert markdown inside card to HTML // Convert markdown inside card to HTML
const htmlContent = markdownToHtml(cardContent.trim()); const htmlContent = markdownToHtml(cardContent.trim());
return `<div class="${cardClass}" style="${bgStyle}">${htmlContent}</div>`; return `<div class="${cardClass}" style="${bgStyle}">${htmlContent}</div>`;
}); });
// Parse button shortcodes - new [button:style](url)Text[/button] syntax // Parse button shortcodes - new [button:style](url)Text[/button] syntax
parsed = parsed.replace(/\[button:(\w+)\]\(([^)]+)\)([^\[]+)\[\/button\]/g, (match, style, url, text) => { parsed = parsed.replace(/\[button:(\w+)\]\(([^)]+)\)([^\[]+)\[\/button\]/g, (match, style, url, text) => {
const buttonClass = style === 'outline' ? 'button-outline' : 'button'; const buttonClass = style === 'outline' ? 'button-outline' : 'button';
return `<p style="text-align: center;"><a href="${url}" class="${buttonClass}">${text.trim()}</a></p>`; return `<p style="text-align: center;"><a href="${url}" class="${buttonClass}">${text.trim()}</a></p>`;
}); });
// Parse button shortcodes - old [button url="..."]Text[/button] syntax (backward compatibility) // Parse button shortcodes - old [button url="..."]Text[/button] syntax (backward compatibility)
parsed = parsed.replace(/\[button\s+url=["']([^"']+)["'](?:\s+style=["'](solid|outline)["'])?\]([^\[]+)\[\/button\]/g, (match, url, style, text) => { parsed = parsed.replace(/\[button\s+url=["']([^"']+)["'](?:\s+style=["'](solid|outline)["'])?\]([^\[]+)\[\/button\]/g, (match, url, style, text) => {
const buttonClass = style === 'outline' ? 'button-outline' : 'button'; const buttonClass = style === 'outline' ? 'button-outline' : 'button';
return `<p style="text-align: center;"><a href="${url}" class="${buttonClass}">${text.trim()}</a></p>`; return `<p style="text-align: center;"><a href="${url}" class="${buttonClass}">${text.trim()}</a></p>`;
}); });
return parsed; return parsed;
}; };
@@ -223,19 +177,19 @@ export default function EditTemplate() {
const generatePreviewHTML = () => { const generatePreviewHTML = () => {
// Convert markdown to HTML for preview // Convert markdown to HTML for preview
let previewBody = parseCardsForPreview(markdownContent); let previewBody = parseCardsForPreview(markdownContent);
// Replace store-identity variables with actual data // Replace store-identity variables with actual data
const storeVariables: { [key: string]: string } = { const storeVariables: { [key: string]: string } = {
store_name: 'My WordPress Store', store_name: 'My WordPress Store',
store_url: window.location.origin, store_url: window.location.origin,
store_email: 'store@example.com', store_email: 'store@example.com',
}; };
Object.entries(storeVariables).forEach(([key, value]) => { Object.entries(storeVariables).forEach(([key, value]) => {
const regex = new RegExp(`\\{${key}\\}`, 'g'); const regex = new RegExp(`\\{${key}\\}`, 'g');
previewBody = previewBody.replace(regex, value); previewBody = previewBody.replace(regex, value);
}); });
// Replace dynamic variables with sample data (not just highlighting) // Replace dynamic variables with sample data (not just highlighting)
const sampleData: { [key: string]: string } = { const sampleData: { [key: string]: string } = {
order_number: '12345', order_number: '12345',
@@ -311,23 +265,23 @@ export default function EditTemplate() {
store_email: 'store@example.com', store_email: 'store@example.com',
support_email: 'support@example.com', support_email: 'support@example.com',
}; };
Object.keys(sampleData).forEach((key) => { Object.keys(sampleData).forEach((key) => {
const regex = new RegExp(`\\{${key}\\}`, 'g'); const regex = new RegExp(`\\{${key}\\}`, 'g');
previewBody = previewBody.replace(regex, sampleData[key]); previewBody = previewBody.replace(regex, sampleData[key]);
}); });
// Highlight variables that don't have sample data // Highlight variables that don't have sample data
availableVariables.forEach(key => { variableKeys.forEach((key: string) => {
if (!storeVariables[key] && !sampleData[key]) { if (!storeVariables[key] && !sampleData[key]) {
const sampleValue = `<span style="background: #fef3c7; padding: 2px 4px; border-radius: 2px;">[${key}]</span>`; const sampleValue = `<span style="background: #fef3c7; padding: 2px 4px; border-radius: 2px;">[${key}]</span>`;
previewBody = previewBody.replace(new RegExp(`\\{${key}\\}`, 'g'), sampleValue); previewBody = previewBody.replace(new RegExp(`\\{${key}\\}`, 'g'), sampleValue);
} }
}); });
// Parse [card] tags // Parse [card] tags
previewBody = parseCardsForPreview(previewBody); previewBody = parseCardsForPreview(previewBody);
// Get email settings for preview // Get email settings for preview
const settings = emailSettings || {}; const settings = emailSettings || {};
const primaryColor = settings.primary_color || '#7f54b3'; const primaryColor = settings.primary_color || '#7f54b3';
@@ -342,10 +296,10 @@ export default function EditTemplate() {
const headerText = settings.header_text || 'My WordPress Store'; const headerText = settings.header_text || 'My WordPress Store';
const footerText = settings.footer_text || `© ${new Date().getFullYear()} My WordPress Store. All rights reserved.`; const footerText = settings.footer_text || `© ${new Date().getFullYear()} My WordPress Store. All rights reserved.`;
const socialLinks = settings.social_links || []; const socialLinks = settings.social_links || [];
// Replace {current_year} in footer // Replace {current_year} in footer
const processedFooter = footerText.replace('{current_year}', new Date().getFullYear().toString()); const processedFooter = footerText.replace('{current_year}', new Date().getFullYear().toString());
// Generate social icons HTML with PNG images // Generate social icons HTML with PNG images
const pluginUrl = const pluginUrl =
(window as any).woonoowData?.pluginUrl || (window as any).woonoowData?.pluginUrl ||
@@ -360,7 +314,7 @@ export default function EditTemplate() {
`).join('')} `).join('')}
</div> </div>
` : ''; ` : '';
return ` return `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@@ -416,7 +370,7 @@ export default function EditTemplate() {
</html> </html>
`; `;
}; };
// Helper function to get social icon emoji // Helper function to get social icon emoji
const getSocialIcon = (platform: string) => { const getSocialIcon = (platform: string) => {
const icons: Record<string, string> = { const icons: Record<string, string> = {
@@ -492,91 +446,91 @@ export default function EditTemplate() {
} }
> >
<Card> <Card>
<CardContent className="pt-6 space-y-6"> <CardContent className="pt-6 space-y-6">
{/* Subject */} {/* Subject */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="subject">{__('Subject / Title')}</Label> <Label htmlFor="subject">{__('Subject / Title')}</Label>
<Input <Input
id="subject" id="subject"
value={subject} value={subject}
onChange={(e) => setSubject(e.target.value)} onChange={(e) => setSubject(e.target.value)}
placeholder={__('Enter notification subject')} placeholder={__('Enter notification subject')}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{channelId === 'email' {channelId === 'email'
? __('Email subject line') ? __('Email subject line')
: __('Push notification title')} : __('Push notification title')}
</p> </p>
</div>
{/* Body */}
<div className="space-y-4">
{/* Three-tab system: Preview | Visual | Markdown */}
<div className="flex items-center justify-between">
<Label>{__('Message Body')}</Label>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-auto">
<TabsList className="grid grid-cols-3">
<TabsTrigger value="preview" className="flex items-center gap-1 text-xs">
<Eye className="h-3 w-3" />
{__('Preview')}
</TabsTrigger>
<TabsTrigger value="visual" className="flex items-center gap-1 text-xs">
<Edit className="h-3 w-3" />
{__('Visual')}
</TabsTrigger>
<TabsTrigger value="markdown" className="flex items-center gap-1 text-xs">
<FileText className="h-3 w-3" />
{__('Markdown')}
</TabsTrigger>
</TabsList>
</Tabs>
</div> </div>
{/* Body */} {/* Preview Tab */}
<div className="space-y-4"> {activeTab === 'preview' && (
{/* Three-tab system: Preview | Visual | Markdown */} <div className="border rounded-md overflow-hidden">
<div className="flex items-center justify-between"> <iframe
<Label>{__('Message Body')}</Label> srcDoc={generatePreviewHTML()}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-auto"> className="w-full min-h-[600px] overflow-hidden bg-white"
<TabsList className="grid grid-cols-3"> title={__('Email Preview')}
<TabsTrigger value="preview" className="flex items-center gap-1 text-xs"> />
<Eye className="h-3 w-3" />
{__('Preview')}
</TabsTrigger>
<TabsTrigger value="visual" className="flex items-center gap-1 text-xs">
<Edit className="h-3 w-3" />
{__('Visual')}
</TabsTrigger>
<TabsTrigger value="markdown" className="flex items-center gap-1 text-xs">
<FileText className="h-3 w-3" />
{__('Markdown')}
</TabsTrigger>
</TabsList>
</Tabs>
</div> </div>
)}
{/* Preview Tab */}
{activeTab === 'preview' && ( {/* Visual Tab */}
<div className="border rounded-md overflow-hidden"> {activeTab === 'visual' && (
<iframe <div>
srcDoc={generatePreviewHTML()} <EmailBuilder
className="w-full min-h-[600px] overflow-hidden bg-white" blocks={blocks}
title={__('Email Preview')} onChange={handleBlocksChange}
/> variables={variableKeys}
</div> />
)} <p className="text-xs text-muted-foreground mt-2">
{__('Build your email visually. Add blocks, edit content, and switch to Preview to see your branding.')}
{/* Visual Tab */} </p>
{activeTab === 'visual' && ( </div>
<div> )}
<EmailBuilder
blocks={blocks} {/* Markdown Tab */}
onChange={handleBlocksChange} {activeTab === 'markdown' && (
variables={variableKeys} <div className="space-y-2">
/> <CodeEditor
<p className="text-xs text-muted-foreground mt-2"> value={markdownContent}
{__('Build your email visually. Add blocks, edit content, and switch to Preview to see your branding.')} onChange={handleMarkdownChange}
</p> placeholder={__('Write in Markdown... Easy and mobile-friendly!')}
</div> supportMarkdown={true}
)} />
<p className="text-xs text-muted-foreground">
{/* Markdown Tab */} {__('Write in Markdown - easy to type, even on mobile! Use **bold**, ## headings, [card]...[/card], etc.')}
{activeTab === 'markdown' && ( </p>
<div className="space-y-2"> <p className="text-xs text-muted-foreground">
<CodeEditor {__('All changes are automatically synced between Visual and Markdown modes.')}
value={markdownContent} </p>
onChange={handleMarkdownChange} </div>
placeholder={__('Write in Markdown... Easy and mobile-friendly!')} )}
supportMarkdown={true} </div>
/> </CardContent>
<p className="text-xs text-muted-foreground"> </Card>
{__('Write in Markdown - easy to type, even on mobile! Use **bold**, ## headings, [card]...[/card], etc.')}
</p>
<p className="text-xs text-muted-foreground">
{__('All changes are automatically synced between Visual and Markdown modes.')}
</p>
</div>
)}
</div>
</CardContent>
</Card>
</SettingsLayout> </SettingsLayout>
); );
} }

View File

@@ -69,7 +69,7 @@ class NotificationsController {
], ],
]); ]);
// GET/PUT /woonoow/v1/notifications/templates/:eventId/:channelId // GET/POST /woonoow/v1/notifications/templates/:eventId/:channelId
register_rest_route($this->namespace, '/' . $this->rest_base . '/templates/(?P<eventId>[a-zA-Z0-9_-]+)/(?P<channelId>[a-zA-Z0-9_-]+)', [ register_rest_route($this->namespace, '/' . $this->rest_base . '/templates/(?P<eventId>[a-zA-Z0-9_-]+)/(?P<channelId>[a-zA-Z0-9_-]+)', [
[ [
'methods' => 'GET', 'methods' => 'GET',
@@ -77,7 +77,7 @@ class NotificationsController {
'permission_callback' => [$this, 'check_permission'], 'permission_callback' => [$this, 'check_permission'],
], ],
[ [
'methods' => 'PUT', 'methods' => 'POST',
'callback' => [$this, 'save_template'], 'callback' => [$this, 'save_template'],
'permission_callback' => [$this, 'check_permission'], 'permission_callback' => [$this, 'check_permission'],
], ],
@@ -486,6 +486,9 @@ class NotificationsController {
} }
} }
// Add available variables for this event (contextual)
$template['available_variables'] = EventRegistry::get_variables_for_event($event_id, $recipient_type);
return new WP_REST_Response($template, 200); return new WP_REST_Response($template, 200);
} }

View File

@@ -371,4 +371,150 @@ class EventRegistry {
public static function event_exists($event_id, $recipient_type) { public static function event_exists($event_id, $recipient_type) {
return self::get_event($event_id, $recipient_type) !== null; return self::get_event($event_id, $recipient_type) !== null;
} }
/**
* Get variables available for a specific event
*
* Returns both common variables and event-specific variables
*
* @param string $event_id Event ID
* @param string $recipient_type Recipient type
* @return array Array of variable definitions with key => description
*/
public static function get_variables_for_event($event_id, $recipient_type = 'customer') {
// Common variables available for ALL events
$common = [
'{site_name}' => __('Store/Site name', 'woonoow'),
'{site_title}' => __('Site title', 'woonoow'),
'{store_url}' => __('Store URL', 'woonoow'),
'{shop_url}' => __('Shop page URL', 'woonoow'),
'{my_account_url}' => __('My Account page URL', 'woonoow'),
'{login_url}' => __('Login page URL', 'woonoow'),
'{support_email}' => __('Support email address', 'woonoow'),
'{current_year}' => __('Current year', 'woonoow'),
'{current_date}' => __('Current date', 'woonoow'),
];
// Customer variables (for customer-facing events)
$customer_vars = [
'{customer_name}' => __('Customer full name', 'woonoow'),
'{customer_first_name}' => __('Customer first name', 'woonoow'),
'{customer_last_name}' => __('Customer last name', 'woonoow'),
'{customer_email}' => __('Customer email', 'woonoow'),
'{customer_phone}' => __('Customer phone', 'woonoow'),
];
// Order variables (for order-related events)
$order_vars = [
'{order_id}' => __('Order ID/number', 'woonoow'),
'{order_number}' => __('Order number', 'woonoow'),
'{order_date}' => __('Order date', 'woonoow'),
'{order_total}' => __('Order total', 'woonoow'),
'{order_subtotal}' => __('Order subtotal', 'woonoow'),
'{order_tax}' => __('Order tax', 'woonoow'),
'{order_shipping}' => __('Shipping cost', 'woonoow'),
'{order_discount}' => __('Discount amount', 'woonoow'),
'{order_status}' => __('Order status', 'woonoow'),
'{order_url}' => __('Order details URL', 'woonoow'),
'{order_items_table}' => __('Order items table (HTML)', 'woonoow'),
'{billing_address}' => __('Billing address', 'woonoow'),
'{shipping_address}' => __('Shipping address', 'woonoow'),
'{payment_method}' => __('Payment method', 'woonoow'),
'{payment_status}' => __('Payment status', 'woonoow'),
'{shipping_method}' => __('Shipping method', 'woonoow'),
];
// Shipping/tracking variables (for shipped/delivered events)
$shipping_vars = [
'{tracking_number}' => __('Tracking number', 'woonoow'),
'{tracking_url}' => __('Tracking URL', 'woonoow'),
'{shipping_carrier}' => __('Shipping carrier', 'woonoow'),
'{estimated_delivery}' => __('Estimated delivery date', 'woonoow'),
];
// Product variables (for stock alerts)
$product_vars = [
'{product_name}' => __('Product name', 'woonoow'),
'{product_sku}' => __('Product SKU', 'woonoow'),
'{product_url}' => __('Product URL', 'woonoow'),
'{product_price}' => __('Product price', 'woonoow'),
'{stock_quantity}' => __('Stock quantity', 'woonoow'),
];
// Newsletter variables
$newsletter_vars = [
'{subscriber_email}' => __('Subscriber email', 'woonoow'),
'{subscriber_name}' => __('Subscriber name', 'woonoow'),
'{unsubscribe_url}' => __('Unsubscribe link', 'woonoow'),
];
// Build variables based on event ID and category
$event = self::get_event($event_id, $recipient_type);
// If event not found, try to match by just event_id
if (!$event) {
$all_events = self::get_all_events();
foreach ($all_events as $e) {
if ($e['id'] === $event_id) {
$event = $e;
break;
}
}
}
// Start with common vars
$variables = $common;
// Add category-specific vars
if ($event) {
$category = $event['category'] ?? '';
// Add customer vars for customer-facing events
if (($event['recipient_type'] ?? '') === 'customer') {
$variables = array_merge($variables, $customer_vars);
}
// Add based on category
switch ($category) {
case 'orders':
$variables = array_merge($variables, $order_vars);
// Add tracking for completed/shipped events
if (in_array($event_id, ['order_completed', 'order_shipped', 'order_delivered'])) {
$variables = array_merge($variables, $shipping_vars);
}
break;
case 'products':
$variables = array_merge($variables, $product_vars);
break;
case 'marketing':
$variables = array_merge($variables, $newsletter_vars);
// Add campaign-specific for newsletter_campaign
if ($event_id === 'newsletter_campaign') {
$variables['{content}'] = __('Campaign content', 'woonoow');
$variables['{campaign_title}'] = __('Campaign title', 'woonoow');
}
break;
case 'customers':
$variables = array_merge($variables, $customer_vars);
// Add account-specific vars
if ($event_id === 'new_customer') {
$variables['{user_temp_password}'] = __('Temporary password', 'woonoow');
}
break;
}
// Add event-specific variables if defined
if (!empty($event['variables'])) {
$variables = array_merge($variables, $event['variables']);
}
}
// Sort alphabetically for easier browsing
ksort($variables);
return $variables;
}
} }