feat: product page layout toggle (flat/card), fix email shortcode rendering
- Add layout_style setting (flat default) to product appearance
- AppearanceController: sanitize & persist layout_style, add to default settings
- Admin SPA: Layout Style select in Appearance > Product
- Customer SPA: useEffect targets <main> bg-white in flat mode (full-width),
card mode uses per-section white floating cards on gray background
- Accordion sections styled per mode: flat=border-t dividers, card=white cards
- Fix email shortcode gaps (EmailRenderer, EmailManager)
- Add missing variables: return_url, contact_url, account_url (alias),
payment_error_reason, order_items_list (alias for order_items_table)
- Fix customer_note extra_data key mismatch (note → customer_note)
- Pass low_stock_threshold via extra_data in low_stock email send
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Email Manager
|
||||
*
|
||||
@@ -9,37 +10,41 @@
|
||||
|
||||
namespace WooNooW\Core\Notifications;
|
||||
|
||||
class EmailManager {
|
||||
|
||||
class EmailManager
|
||||
{
|
||||
|
||||
/**
|
||||
* Instance
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*/
|
||||
public static function instance() {
|
||||
public static function instance()
|
||||
{
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
private function __construct()
|
||||
{
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize hooks
|
||||
*/
|
||||
private function init_hooks() {
|
||||
private function init_hooks()
|
||||
{
|
||||
// Disable WooCommerce emails to prevent duplicates
|
||||
add_action('woocommerce_email', [$this, 'disable_wc_emails'], 1);
|
||||
|
||||
|
||||
// Hook into WooCommerce order status changes
|
||||
add_action('woocommerce_order_status_pending_to_processing', [$this, 'send_order_processing_email'], 10, 2);
|
||||
add_action('woocommerce_order_status_pending_to_completed', [$this, 'send_order_completed_email'], 10, 2);
|
||||
@@ -50,47 +55,49 @@ class EmailManager {
|
||||
add_action('woocommerce_order_status_cancelled', [$this, 'send_order_cancelled_email'], 10, 2);
|
||||
add_action('woocommerce_order_status_refunded', [$this, 'send_order_refunded_email'], 10, 2);
|
||||
add_action('woocommerce_order_fully_refunded', [$this, 'send_order_refunded_email'], 10, 2);
|
||||
|
||||
|
||||
// New order notification for admin
|
||||
add_action('woocommerce_new_order', [$this, 'send_new_order_admin_email'], 10, 1);
|
||||
|
||||
|
||||
// Customer note
|
||||
add_action('woocommerce_new_customer_note', [$this, 'send_customer_note_email'], 10, 1);
|
||||
|
||||
|
||||
// New customer account
|
||||
add_action('woocommerce_created_customer', [$this, 'send_new_customer_email'], 10, 3);
|
||||
|
||||
|
||||
// Password reset - intercept WordPress default email and use our template
|
||||
add_filter('retrieve_password_message', [$this, 'handle_password_reset_email'], 10, 4);
|
||||
|
||||
|
||||
// Low stock / Out of stock
|
||||
add_action('woocommerce_low_stock', [$this, 'send_low_stock_email'], 10, 1);
|
||||
add_action('woocommerce_no_stock', [$this, 'send_out_of_stock_email'], 10, 1);
|
||||
add_action('woocommerce_product_set_stock', [$this, 'check_stock_levels'], 10, 1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if WooNooW notification system is enabled
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_enabled() {
|
||||
public static function is_enabled()
|
||||
{
|
||||
// Check global notification system mode
|
||||
$system_mode = get_option('woonoow_notification_system_mode', 'woonoow');
|
||||
return $system_mode === 'woonoow';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disable WooCommerce default emails
|
||||
*
|
||||
* @param WC_Emails $email_class
|
||||
*/
|
||||
public function disable_wc_emails($email_class) {
|
||||
public function disable_wc_emails($email_class)
|
||||
{
|
||||
// Only disable WC emails if WooNooW system is enabled
|
||||
if (!self::is_enabled()) {
|
||||
return; // Keep WC emails if WooNooW system disabled
|
||||
}
|
||||
|
||||
|
||||
// Disable all WooCommerce transactional emails
|
||||
$emails_to_disable = [
|
||||
'WC_Email_New_Order', // Admin: New order
|
||||
@@ -105,181 +112,188 @@ class EmailManager {
|
||||
'WC_Email_Customer_Reset_Password', // Customer: Reset password
|
||||
'WC_Email_Customer_New_Account', // Customer: New account
|
||||
];
|
||||
|
||||
|
||||
foreach ($emails_to_disable as $email_id) {
|
||||
add_filter('woocommerce_email_enabled_' . strtolower(str_replace('WC_Email_', '', $email_id)), '__return_false');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send order processing email
|
||||
*
|
||||
* @param int $order_id
|
||||
* @param WC_Order $order
|
||||
*/
|
||||
public function send_order_processing_email($order_id, $order = null) {
|
||||
public function send_order_processing_email($order_id, $order = null)
|
||||
{
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
}
|
||||
|
||||
|
||||
if (!$order) {
|
||||
$order = wc_get_order($order_id);
|
||||
}
|
||||
|
||||
|
||||
if (!$order) {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if event is enabled
|
||||
if (!$this->is_event_enabled('order_processing', 'email', 'customer')) {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
}
|
||||
|
||||
|
||||
// Send email
|
||||
$this->send_email('order_processing', 'customer', $order);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send order completed email
|
||||
*
|
||||
* @param int $order_id
|
||||
* @param WC_Order $order
|
||||
*/
|
||||
public function send_order_completed_email($order_id, $order = null) {
|
||||
public function send_order_completed_email($order_id, $order = null)
|
||||
{
|
||||
if (!$order) {
|
||||
$order = wc_get_order($order_id);
|
||||
}
|
||||
|
||||
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if event is enabled
|
||||
if (!$this->is_event_enabled('order_completed', 'email', 'customer')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Send email
|
||||
$this->send_email('order_completed', 'customer', $order);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send order on-hold email
|
||||
*
|
||||
* @param int $order_id
|
||||
* @param WC_Order $order
|
||||
*/
|
||||
public function send_order_on_hold_email($order_id, $order = null) {
|
||||
public function send_order_on_hold_email($order_id, $order = null)
|
||||
{
|
||||
if (!$order) {
|
||||
$order = wc_get_order($order_id);
|
||||
}
|
||||
|
||||
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if event is enabled
|
||||
if (!$this->is_event_enabled('order_processing', 'email', 'customer')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Send email (use processing template for on-hold)
|
||||
$this->send_email('order_processing', 'customer', $order);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send order cancelled email
|
||||
*
|
||||
* @param int $order_id
|
||||
* @param WC_Order $order
|
||||
*/
|
||||
public function send_order_cancelled_email($order_id, $order = null) {
|
||||
public function send_order_cancelled_email($order_id, $order = null)
|
||||
{
|
||||
if (!$order) {
|
||||
$order = wc_get_order($order_id);
|
||||
}
|
||||
|
||||
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Send to admin
|
||||
if ($this->is_event_enabled('order_cancelled', 'email', 'staff')) {
|
||||
$this->send_email('order_cancelled', 'staff', $order);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send order refunded email
|
||||
*
|
||||
* @param int $order_id
|
||||
* @param WC_Order $order
|
||||
*/
|
||||
public function send_order_refunded_email($order_id, $order = null) {
|
||||
public function send_order_refunded_email($order_id, $order = null)
|
||||
{
|
||||
if (!$order) {
|
||||
$order = wc_get_order($order_id);
|
||||
}
|
||||
|
||||
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if event is enabled
|
||||
if (!$this->is_event_enabled('order_refunded', 'email', 'customer')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Send email
|
||||
$this->send_email('order_refunded', 'customer', $order);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send new order admin email
|
||||
*
|
||||
* @param int $order_id
|
||||
*/
|
||||
public function send_new_order_admin_email($order_id) {
|
||||
public function send_new_order_admin_email($order_id)
|
||||
{
|
||||
$order = wc_get_order($order_id);
|
||||
|
||||
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if event is enabled
|
||||
if (!$this->is_event_enabled('order_placed', 'email', 'staff')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Send email
|
||||
$this->send_email('order_placed', 'staff', $order);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send customer note email
|
||||
*
|
||||
* @param array $args
|
||||
*/
|
||||
public function send_customer_note_email($args) {
|
||||
public function send_customer_note_email($args)
|
||||
{
|
||||
$order = wc_get_order($args['order_id']);
|
||||
|
||||
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if event is enabled
|
||||
if (!$this->is_event_enabled('customer_note', 'email', 'customer')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send email with note data
|
||||
$this->send_email('customer_note', 'customer', $order, ['note' => $args['customer_note']]);
|
||||
|
||||
// Send email with note data — key must match {customer_note} variable in template
|
||||
$this->send_email('customer_note', 'customer', $order, ['customer_note' => $args['customer_note']]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send new customer email
|
||||
*
|
||||
@@ -287,14 +301,15 @@ class EmailManager {
|
||||
* @param array $new_customer_data
|
||||
* @param bool $password_generated
|
||||
*/
|
||||
public function send_new_customer_email($customer_id, $new_customer_data = [], $password_generated = false) {
|
||||
public function send_new_customer_email($customer_id, $new_customer_data = [], $password_generated = false)
|
||||
{
|
||||
// Check if event is enabled
|
||||
if (!$this->is_event_enabled('new_customer', 'email', 'customer')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$customer = new \WC_Customer($customer_id);
|
||||
|
||||
|
||||
// Send email
|
||||
$this->send_email('new_customer', 'customer', $customer, [
|
||||
'password_generated' => $password_generated,
|
||||
@@ -302,7 +317,7 @@ class EmailManager {
|
||||
'user_pass' => $new_customer_data['user_pass'] ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle password reset email - intercept WordPress default and use our template
|
||||
*
|
||||
@@ -312,30 +327,31 @@ class EmailManager {
|
||||
* @param WP_User $user_data User object
|
||||
* @return string Empty string to prevent WordPress sending default email
|
||||
*/
|
||||
public function handle_password_reset_email($message, $key, $user_login, $user_data) {
|
||||
public function handle_password_reset_email($message, $key, $user_login, $user_data)
|
||||
{
|
||||
// Check if WooNooW notification system is enabled
|
||||
if (!self::is_enabled()) {
|
||||
return $message; // Use WordPress default
|
||||
}
|
||||
|
||||
|
||||
// Check if event is enabled
|
||||
if (!$this->is_event_enabled('password_reset', 'email', 'customer')) {
|
||||
return $message; // Use WordPress default
|
||||
}
|
||||
|
||||
|
||||
// Build reset URL - use SPA page from appearance settings
|
||||
// The SPA page (e.g., /store/) loads customer-spa which has /reset-password route
|
||||
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
|
||||
$use_browser_router = $appearance_settings['general']['use_browser_router'] ?? true;
|
||||
|
||||
|
||||
if ($spa_page_id > 0) {
|
||||
$spa_url = get_permalink($spa_page_id);
|
||||
} else {
|
||||
// Fallback to home URL if SPA page not configured
|
||||
$spa_url = home_url('/');
|
||||
}
|
||||
|
||||
|
||||
// Build SPA reset password URL
|
||||
// Use path format for BrowserRouter (SEO), hash format for HashRouter (legacy)
|
||||
if ($use_browser_router) {
|
||||
@@ -345,7 +361,7 @@ class EmailManager {
|
||||
// Hash format: /store/#/reset-password?key=KEY&login=LOGIN
|
||||
$reset_link = rtrim($spa_url, '/') . '#/reset-password?key=' . $key . '&login=' . rawurlencode($user_login);
|
||||
}
|
||||
|
||||
|
||||
// Create a pseudo WC_Customer for template rendering
|
||||
$customer = null;
|
||||
if (class_exists('WC_Customer')) {
|
||||
@@ -355,14 +371,14 @@ class EmailManager {
|
||||
$customer = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Send our custom email
|
||||
$this->send_password_reset_email($user_data, $key, $reset_link, $customer);
|
||||
|
||||
|
||||
// Return empty string to prevent WordPress from sending its default plain-text email
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send password reset email using our template
|
||||
*
|
||||
@@ -371,10 +387,11 @@ class EmailManager {
|
||||
* @param string $reset_link Full reset link URL
|
||||
* @param WC_Customer|null $customer WooCommerce customer object if available
|
||||
*/
|
||||
private function send_password_reset_email($user, $key, $reset_link, $customer = null) {
|
||||
private function send_password_reset_email($user, $key, $reset_link, $customer = null)
|
||||
{
|
||||
// Get email renderer
|
||||
$renderer = EmailRenderer::instance();
|
||||
|
||||
|
||||
// Build extra data for template variables
|
||||
$extra_data = [
|
||||
'reset_key' => $key,
|
||||
@@ -384,80 +401,86 @@ class EmailManager {
|
||||
'customer_name' => $user->display_name ?: $user->user_login,
|
||||
'customer_email' => $user->user_email,
|
||||
];
|
||||
|
||||
|
||||
// Use WC_Customer if available for better template rendering
|
||||
$data = $customer ?: $user;
|
||||
|
||||
|
||||
// Render email
|
||||
$email = $renderer->render('password_reset', 'customer', $data, $extra_data);
|
||||
|
||||
|
||||
if (!$email) {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Send email via wp_mail
|
||||
$headers = [
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
'From: ' . get_bloginfo('name') . ' <' . get_option('admin_email') . '>',
|
||||
];
|
||||
|
||||
|
||||
$sent = wp_mail($email['to'], $email['subject'], $email['body'], $headers);
|
||||
|
||||
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
}
|
||||
|
||||
|
||||
// Log email sent
|
||||
do_action('woonoow_email_sent', 'password_reset', 'customer', $email);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send low stock email
|
||||
*
|
||||
* @param WC_Product $product
|
||||
*/
|
||||
public function send_low_stock_email($product) {
|
||||
public function send_low_stock_email($product)
|
||||
{
|
||||
// Check if event is enabled
|
||||
if (!$this->is_event_enabled('low_stock', 'email', 'staff')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send email
|
||||
$this->send_email('low_stock', 'staff', $product);
|
||||
|
||||
// Pass low_stock_threshold so template can display it
|
||||
$low_stock_threshold = get_option('woocommerce_notify_low_stock_amount', 2);
|
||||
$this->send_email('low_stock', 'staff', $product, [
|
||||
'low_stock_threshold' => $low_stock_threshold,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send out of stock email
|
||||
*
|
||||
* @param WC_Product $product
|
||||
*/
|
||||
public function send_out_of_stock_email($product) {
|
||||
public function send_out_of_stock_email($product)
|
||||
{
|
||||
// Check if event is enabled
|
||||
if (!$this->is_event_enabled('out_of_stock', 'email', 'staff')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Send email
|
||||
$this->send_email('out_of_stock', 'staff', $product);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check stock levels when product stock is updated
|
||||
*
|
||||
* @param WC_Product $product
|
||||
*/
|
||||
public function check_stock_levels($product) {
|
||||
public function check_stock_levels($product)
|
||||
{
|
||||
$stock = $product->get_stock_quantity();
|
||||
$low_stock_threshold = get_option('woocommerce_notify_low_stock_amount', 2);
|
||||
|
||||
|
||||
if ($stock <= 0) {
|
||||
$this->send_out_of_stock_email($product);
|
||||
} elseif ($stock <= $low_stock_threshold) {
|
||||
$this->send_low_stock_email($product);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if event is enabled
|
||||
*
|
||||
@@ -466,14 +489,15 @@ class EmailManager {
|
||||
* @param string $recipient_type
|
||||
* @return bool
|
||||
*/
|
||||
private function is_event_enabled($event_id, $channel_id, $recipient_type) {
|
||||
private function is_event_enabled($event_id, $channel_id, $recipient_type)
|
||||
{
|
||||
$settings = get_option('woonoow_notification_settings', []);
|
||||
|
||||
|
||||
// Check if event exists and channel is configured
|
||||
if (isset($settings['events'][$event_id]['channels'][$channel_id])) {
|
||||
return $settings['events'][$event_id]['channels'][$channel_id]['enabled'] ?? false;
|
||||
}
|
||||
|
||||
|
||||
// Default: enable email notifications if not explicitly configured
|
||||
// This allows the plugin to work out-of-the-box with default templates
|
||||
if ($channel_id === 'email') {
|
||||
@@ -481,10 +505,10 @@ class EmailManager {
|
||||
}
|
||||
return true; // Enable by default
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send email
|
||||
*
|
||||
@@ -493,36 +517,37 @@ class EmailManager {
|
||||
* @param mixed $data
|
||||
* @param array $extra_data
|
||||
*/
|
||||
private function send_email($event_id, $recipient_type, $data, $extra_data = []) {
|
||||
private function send_email($event_id, $recipient_type, $data, $extra_data = [])
|
||||
{
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
}
|
||||
|
||||
|
||||
// Get email renderer
|
||||
$renderer = EmailRenderer::instance();
|
||||
|
||||
|
||||
// Render email
|
||||
$email = $renderer->render($event_id, $recipient_type, $data, $extra_data);
|
||||
|
||||
|
||||
if (!$email) {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
}
|
||||
|
||||
|
||||
// Send email via wp_mail
|
||||
$headers = [
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
'From: ' . get_bloginfo('name') . ' <' . get_option('admin_email') . '>',
|
||||
];
|
||||
|
||||
|
||||
$sent = wp_mail($email['to'], $email['subject'], $email['body'], $headers);
|
||||
|
||||
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
}
|
||||
|
||||
|
||||
// Log email sent
|
||||
do_action('woonoow_email_sent', $event_id, $recipient_type, $email);
|
||||
}
|
||||
|
||||
@@ -227,6 +227,7 @@ class EmailRenderer
|
||||
'payment_method' => $data->get_payment_method_title(),
|
||||
'payment_status' => $data->get_status(),
|
||||
'payment_date' => $payment_date,
|
||||
'payment_error_reason' => $data->get_meta('_payment_error_reason') ?: 'Payment declined',
|
||||
'transaction_id' => $data->get_transaction_id() ?: 'N/A',
|
||||
'shipping_method' => $data->get_shipping_method(),
|
||||
'estimated_delivery' => $estimated_delivery,
|
||||
@@ -239,9 +240,12 @@ class EmailRenderer
|
||||
'billing_address' => $data->get_formatted_billing_address(),
|
||||
'shipping_address' => $data->get_formatted_shipping_address(),
|
||||
// URLs
|
||||
'review_url' => $data->get_view_order_url(), // Can be customized later
|
||||
'review_url' => $data->get_view_order_url(),
|
||||
'return_url' => $data->get_view_order_url(), // Customers click to initiate return
|
||||
'contact_url' => home_url('/contact'),
|
||||
'shop_url' => get_permalink(wc_get_page_id('shop')),
|
||||
'my_account_url' => get_permalink(wc_get_page_id('myaccount')),
|
||||
'account_url' => get_permalink(wc_get_page_id('myaccount')), // Alias for my_account_url
|
||||
'payment_retry_url' => $data->get_checkout_payment_url(),
|
||||
// Tracking (if available from meta)
|
||||
'tracking_number' => $data->get_meta('_tracking_number') ?: 'N/A',
|
||||
@@ -249,6 +253,7 @@ class EmailRenderer
|
||||
'shipping_carrier' => $data->get_meta('_shipping_carrier') ?: 'Standard Shipping',
|
||||
]);
|
||||
|
||||
|
||||
// Order items table
|
||||
$items_html = '<table class="order-details" style="width: 100%; border-collapse: collapse;">';
|
||||
$items_html .= '<thead><tr>';
|
||||
@@ -277,9 +282,10 @@ class EmailRenderer
|
||||
|
||||
$items_html .= '</tbody></table>';
|
||||
|
||||
// Both naming conventions for compatibility
|
||||
// All naming conventions for compatibility
|
||||
$variables['order_items'] = $items_html;
|
||||
$variables['order_items_table'] = $items_html;
|
||||
$variables['order_items_list'] = $items_html; // Alias used in some templates
|
||||
}
|
||||
|
||||
// Product variables
|
||||
|
||||
Reference in New Issue
Block a user