prefix . 'aieo_waitlist'; } public function register_hooks() { add_action('wp_ajax_aieo_dmm_waitlist_save', array($this, 'ajax_save_settings')); add_action('wp_ajax_aieo_dmm_waitlist_delete', array($this, 'ajax_delete_subscriber')); if (!class_exists('AIEO_Waitlist_Privacy', false) && is_readable(AIEO_DIR . 'includes/dmm/class-aieo-waitlist-privacy.php')) { require_once AIEO_DIR . 'includes/dmm/class-aieo-waitlist-privacy.php'; } if (class_exists('AIEO_Waitlist_Privacy', false)) { add_filter('wp_privacy_personal_data_exporters', array('AIEO_Waitlist_Privacy', 'register_exporter')); add_filter('wp_privacy_personal_data_erasers', array('AIEO_Waitlist_Privacy', 'register_eraser')); } add_action('plugins_loaded', array($this, 'maybe_install'), 20); if ( ! defined( 'AIEO_RUNTIME_OWNS_AJAX' ) ) { add_action('wp_ajax_aieo_waitlist_subscribe', array($this, 'ajax_subscribe')); add_action('wp_ajax_nopriv_aieo_waitlist_subscribe', array($this, 'ajax_subscribe')); add_action('wp_ajax_om_ppe_waitlist_subscribe', array($this, 'ajax_subscribe'), 1); add_action('wp_ajax_nopriv_om_ppe_waitlist_subscribe', array($this, 'ajax_subscribe'), 1); } add_action('woocommerce_product_set_stock_status', array($this, 'on_stock_changed'), 10, 3); add_action('woocommerce_variation_set_stock_status', array($this, 'on_variation_stock_changed'), 10, 3); add_action('aieo/waitlist_notify', array($this, 'as_notify'), 10, 3); $settings = $this->get_settings(); $dash_on = class_exists('AIEO_DMM_Product_Display') && AIEO_DMM_Product_Display::module_is_enabled(self::OPTION_KEY); if ($dash_on && !empty($settings['frontend_enabled']) && $settings['frontend_enabled'] === 'yes') { $is_block_theme = wp_is_block_theme(); $use_wc_hook = (bool) apply_filters('aieo_dmm_waitlist_fse_use_hook', ! $is_block_theme); if ($use_wc_hook) { add_action('woocommerce_simple_add_to_cart', array($this, 'render_form_hook'), 30); add_action('woocommerce_after_variations_form', array($this, 'render_form_hook'), 30); } add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets')); add_shortcode('aieo_waitlist', array($this, 'shortcode_handler')); } } public function is_active() { $stored = $this->get_settings(); if (!isset($stored['frontend_enabled']) || $stored['frontend_enabled'] !== 'yes') { return false; } if (isset($stored['enabled']) && empty($stored['enabled'])) { return false; } return true; } public function maybe_install() { if (get_option(self::SCHEMA_OPTION, '') !== self::SCHEMA_VERSION && AIEO_Schema_Lock::acquire(self::SCHEMA_OPTION)) { $this->install_table(); update_option(self::SCHEMA_OPTION, self::SCHEMA_VERSION, false); } if (!get_option(self::MIGRATED_FLAG, 0)) { $this->migrate_from_ppe(); update_option(self::MIGRATED_FLAG, 1, false); } } private function install_table() { global $wpdb; $table = self::get_table_name(); $charset = aieo_get_charset_collate(); $sql = "CREATE TABLE {$table} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, email varchar(255) NOT NULL, product_id bigint(20) unsigned NOT NULL, variation_id bigint(20) unsigned NOT NULL DEFAULT 0, status varchar(20) NOT NULL DEFAULT 'subscribed', created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, notified_at datetime NULL DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY email_pid_vid (email, product_id, variation_id), KEY product_id (product_id), KEY status (status) ) {$charset};"; if (!function_exists('dbDelta')) { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; } dbDelta($sql); } private function migrate_from_ppe() { global $wpdb; $ppe = $wpdb->prefix . 'om_ppe_waitlist'; $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $ppe)); if ($exists !== $ppe) { return; } $dest = self::get_table_name(); $wpdb->query("INSERT IGNORE INTO {$dest} (email, product_id, variation_id, status, created_at, notified_at) SELECT email, product_id, variation_id, status, created_at, notified_at FROM {$ppe}"); } public function get_settings($raw = false) { $stored = get_option(self::OPTION_KEY, array()); if (!is_array($stored)) { $stored = array(); } $merged = array_merge(array( 'frontend_enabled' => 'no', 'form_title' => '', 'form_email_label' => '', 'form_button_text' => '', 'form_success_message' => '', 'email_subject' => '', 'email_message' => '', 'display_on_backorders' => 'no', 'title_size' => 16, 'text_size' => 14, 'bg_color' => '#f8f9fa', 'border_color' => '#e9ecef', 'border_width' => 1, 'border_radius' => 8, 'btn_bg_color' => '', 'btn_text_color' => '', 'btn_border_color' => '', 'btn_border_width' => '', 'btn_border_radius' => '', ), $stored); return $raw ? $merged : self::fill_i18n_defaults($merged); } private static function i18n_labels() { return array( 'form_title' => __('Email me when this item is back in stock.', 'ai-eshop-optimizer'), 'form_email_label' => __('Your Email Address', 'ai-eshop-optimizer'), 'form_button_text' => __('Notify Me', 'ai-eshop-optimizer'), 'form_success_message' => __('You have been successfully added to our stock waitlist. As soon as new stock becomes available, we will notify you via email.', 'ai-eshop-optimizer'), 'email_subject' => __('Back in Stock: {product_name}', 'ai-eshop-optimizer'), 'email_message' => __('Hello, thanks for your patience — your {product_name} is now back in stock! Stock is limited; this email is not a guarantee you will get one.', 'ai-eshop-optimizer'), ); } private static function fill_i18n_defaults(array $s) { if (!did_action('init')) { return $s; } foreach (self::i18n_labels() as $k => $v) { if (!isset($s[$k]) || '' === trim((string) $s[$k])) { $s[$k] = $v; } } return $s; } public function render_subtab() { $paged = max(1, isset($_GET['waitlist_paged']) ? (int) $_GET['waitlist_paged'] : 1); $per_page = 50; $this->render_template('waitlist.php', array( 'settings' => $this->get_settings(), 'subscribers' => $this->fetch_subscribers($per_page, $paged), 'total' => $this->count_subscribers(), 'per_page' => $per_page, 'current_page' => $paged, )); } public function ajax_save_settings() { AIEO_DMM::verify_ajax_request(); $data = array( 'frontend_enabled' => !empty($_POST['frontend_enabled']) ? 'yes' : 'no', 'form_title' => isset($_POST['form_title']) ? sanitize_text_field(wp_unslash($_POST['form_title'])) : '', 'form_email_label' => isset($_POST['form_email_label']) ? sanitize_text_field(wp_unslash($_POST['form_email_label'])) : '', 'form_button_text' => isset($_POST['form_button_text']) ? sanitize_text_field(wp_unslash($_POST['form_button_text'])) : '', 'form_success_message' => isset($_POST['form_success_message']) ? sanitize_textarea_field(wp_unslash($_POST['form_success_message'])) : '', 'email_subject' => isset($_POST['email_subject']) ? sanitize_text_field(wp_unslash($_POST['email_subject'])) : '', 'email_message' => isset($_POST['email_message']) ? sanitize_textarea_field(wp_unslash($_POST['email_message'])) : '', 'display_on_backorders' => !empty($_POST['display_on_backorders']) ? 'yes' : 'no', ); $num_clamps = array( 'title_size' => array(8, 48), 'text_size' => array(8, 32), 'border_width' => array(0, 10), 'border_radius' => array(0, 40), 'btn_border_width' => array(0, 10), 'btn_border_radius' => array(0, 40), ); foreach ($num_clamps as $k => list($min, $max)) { if (isset($_POST[$k])) { $data[$k] = max($min, min($max, (int) wp_unslash($_POST[$k]))); } } foreach (array('bg_color', 'border_color', 'btn_bg_color', 'btn_text_color', 'btn_border_color') as $k) { if (isset($_POST[$k])) { $v = sanitize_hex_color(wp_unslash((string) $_POST[$k])); if ($v !== null && $v !== '') { $data[$k] = $v; } } } $existing = get_option(self::OPTION_KEY, array()); if (!is_array($existing)) { $existing = array(); } update_option(self::OPTION_KEY, array_merge($existing, $data), false); wp_send_json_success(array('message' => __('Waitlist settings saved.', 'ai-eshop-optimizer'))); } public function ajax_delete_subscriber() { AIEO_DMM::verify_ajax_request(); $id = isset($_POST['id']) ? (int) $_POST['id'] : 0; if ($id <= 0) { wp_send_json_error(array('message' => __('Invalid id.', 'ai-eshop-optimizer'))); } global $wpdb; $wpdb->delete(self::get_table_name(), array('id' => $id), array('%d')); if (class_exists('AIEO_Submission_Context') && method_exists('AIEO_Submission_Context', 'forget_leads')) { AIEO_Submission_Context::forget_leads(array(), array('wl:' . $id . ':')); } wp_send_json_success(); } private function fetch_subscribers($per_page, $paged) { global $wpdb; $table = self::get_table_name(); $offset = ($paged - 1) * $per_page; return $wpdb->get_results($wpdb->prepare( "SELECT id, email, product_id, variation_id, status, created_at, notified_at FROM {$table} ORDER BY id DESC LIMIT %d OFFSET %d", $per_page, $offset )); } private function count_subscribers() { global $wpdb; return (int) $wpdb->get_var('SELECT COUNT(*) FROM ' . self::get_table_name()); } public function enqueue_frontend_assets() { if (!is_product()) { return; } $css_rel = 'assets/css/aieo-waitlist.css'; $js_rel = 'assets/js/aieo-waitlist.js'; $css_path = dirname(AIEO_FILE) . '/' . $css_rel; $js_path = dirname(AIEO_FILE) . '/' . $js_rel; $css_ver = file_exists($css_path) ? filemtime($css_path) : AIEO_VERSION; $js_ver = file_exists($js_path) ? filemtime($js_path) : AIEO_VERSION; wp_enqueue_style('aieo-waitlist', plugins_url($css_rel, AIEO_FILE), array(), $css_ver); $s = $this->get_settings(); $btn_bg = trim((string) ($s['btn_bg_color'] ?? '')); $btn_text = trim((string) ($s['btn_text_color'] ?? '')); $btn_border = trim((string) ($s['btn_border_color'] ?? '')); $btn_bw = (string) ($s['btn_border_width'] ?? ''); $btn_br = (string) ($s['btn_border_radius'] ?? ''); $vars = sprintf( '.aieo-waitlist-container{' . '--aieo-wl-bg:%s;' . '--aieo-wl-border:%s;' . '--aieo-wl-border-w:%dpx;' . '--aieo-wl-border-r:%dpx;' . '--aieo-wl-title-size:%dpx;' . '--aieo-wl-text-size:%dpx;' . '--aieo-wl-btn-bg:%s;' . '--aieo-wl-btn-text:%s;' . '--aieo-wl-btn-border:%s;' . '--aieo-wl-btn-border-w:%s;' . '--aieo-wl-btn-border-r:%s;' . '}', esc_attr((string) ($s['bg_color'] ?? '#f8f9fa')), esc_attr((string) ($s['border_color'] ?? '#e9ecef')), (int) ($s['border_width'] ?? 1), (int) ($s['border_radius'] ?? 8), (int) ($s['title_size'] ?? 16), (int) ($s['text_size'] ?? 14), $btn_bg !== '' ? esc_attr($btn_bg) : 'var(--aieo-btn-bg,#0073aa)', $btn_text !== '' ? esc_attr($btn_text) : 'var(--aieo-btn-fg,#ffffff)', $btn_border !== '' ? esc_attr($btn_border) : 'var(--aieo-btn-border,#0073aa)', $btn_bw !== '' ? ((int) $btn_bw . 'px') : 'var(--aieo-btn-border-w,0px)', $btn_br !== '' ? ((int) $btn_br . 'px') : 'var(--aieo-btn-radius,4px)' ); wp_add_inline_style('aieo-waitlist', $vars); $iapi_on = method_exists('AIEO_DMM', 'is_iapi_enabled') && AIEO_DMM::is_iapi_enabled('waitlist'); if ($iapi_on && defined('AIEO_RUNTIME_DIR') && file_exists(AIEO_RUNTIME_DIR . 'assets/js/aieo-waitlist-iapi.js') ) { wp_register_script_module( '@aieo/waitlist-iapi', AIEO_Asset_Min::module_src(AIEO_RUNTIME_DIR . 'assets/js/aieo-waitlist-iapi.js', AIEO_RUNTIME_URI . 'assets/js/aieo-waitlist-iapi.js'), array('@aieo/iapi-bootstrap'), (string) filemtime(AIEO_RUNTIME_DIR . 'assets/js/aieo-waitlist-iapi.js'), array( 'fetchpriority' => 'low' ) ); wp_enqueue_script_module('@aieo/waitlist-iapi'); } else { wp_enqueue_script('aieo-waitlist', plugins_url($js_rel, AIEO_FILE), array('jquery'), $js_ver, array('in_footer' => true, 'strategy' => 'defer')); } $waitlist_data = array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aieo_waitlist'), 'legacyAction' => 'om_ppe_waitlist_subscribe', 'i18n' => array( 'invalidEmail' => __('Please enter a valid email address.', 'ai-eshop-optimizer'), 'genericError' => __('An error occurred. Please try again.', 'ai-eshop-optimizer'), ), ); if ($iapi_on) { add_action('wp_print_footer_scripts', static function () use ($waitlist_data) { echo '' . "\n"; }, 5); } else { wp_localize_script('aieo-waitlist', 'aieoWaitlist', $waitlist_data); } } public function render_form_hook() { global $product; if (!$product || !$this->should_display($product)) { return; } echo $this->render_form($product); } public function shortcode_handler($atts = array()) { if (!is_singular('product')) { return ''; } global $product; if (!$product || !$this->should_display($product)) { return ''; } return $this->render_form($product); } private function should_display($product) { if (!$product || !method_exists($product, 'is_in_stock')) { return false; } if ($product->get_stock_status() === 'onbackorder') { $settings = $this->get_settings(); return !empty($settings['display_on_backorders']) && $settings['display_on_backorders'] === 'yes'; } return !$product->is_in_stock(); } private function render_form($product) { $settings = $this->get_settings(); if (!wp_style_is('aieo-waitlist', 'enqueued')) { $this->enqueue_frontend_assets(); } ob_start(); ?>