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(); ?>

__('Session expired. Reload the page and try again.', 'ai-eshop-optimizer'))); } $email = isset($_POST['email']) ? sanitize_email(wp_unslash($_POST['email'])) : ''; $product_id = isset($_POST['product_id']) ? absint(wp_unslash($_POST['product_id'])) : 0; $variation_id = isset($_POST['variation_id']) ? absint(wp_unslash($_POST['variation_id'])) : 0; if (!is_email($email)) { wp_send_json_error(array('message' => __('Please enter a valid email address.', 'ai-eshop-optimizer'))); } $product = wc_get_product($product_id); if (!$product) { wp_send_json_error(array('message' => __('Invalid product.', 'ai-eshop-optimizer'))); } if ($this->is_subscribed($email, $product_id, $variation_id)) { wp_send_json_error(array('message' => __('You are already subscribed to the stock notification list for this product.', 'ai-eshop-optimizer'))); } global $wpdb; $existing = (int) $wpdb->get_var($wpdb->prepare( "SELECT id FROM " . self::get_table_name() . " WHERE email = %s AND product_id = %d AND variation_id = %d LIMIT 1", $email, $product_id, $variation_id )); if ($existing > 0) { $ok = false !== $wpdb->update(self::get_table_name(), array('status' => 'subscribed', 'notified_at' => null, 'created_at' => current_time('mysql', true)), array('id' => $existing), array('%s', '%s', '%s'), array('%d') ); } else { $ok = (bool) $wpdb->insert(self::get_table_name(), array( 'email' => $email, 'product_id' => $product_id, 'variation_id' => $variation_id, 'status' => 'subscribed', 'created_at' => current_time('mysql', true), ), array('%s', '%d', '%d', '%s', '%s')); } if (!$ok) { wp_send_json_error(array('message' => __('Could not save your subscription. Try again later.', 'ai-eshop-optimizer'))); } do_action('aieo_waitlist_subscribed', $existing > 0 ? $existing : (int) $wpdb->insert_id, $email, $product_id, $variation_id ); $settings = $this->get_settings(); wp_send_json_success(array('message' => $settings['form_success_message'])); } private function is_subscribed($email, $product_id, $variation_id) { global $wpdb; return (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM " . self::get_table_name() . " WHERE email = %s AND product_id = %d AND variation_id = %d AND status = 'subscribed'", $email, $product_id, $variation_id )) > 0; } const NOTIFY_BATCH = 10; public function on_stock_changed($product_id, $stock_status, $product = null) { $this->record_restock((int) $product_id, (string) $stock_status); if ($stock_status !== 'instock') { return; } $this->queue_notify((int) $product_id, 0); } private function record_restock($product_id, $new_status) { if ($product_id <= 0 || $new_status === '') { return; } $prev = (string) get_post_meta($product_id, '_aieo_last_stock_status', true); if ('instock' === $new_status && '' !== $prev && 'instock' !== $prev) { update_post_meta($product_id, '_aieo_restocked_at', gmdate('Y-m-d H:i:s')); } if ($new_status !== $prev) { update_post_meta($product_id, '_aieo_last_stock_status', $new_status); } } public function on_variation_stock_changed($variation_id, $stock_status, $variation = null) { if ($stock_status !== 'instock') { return; } $this->queue_notify(0, (int) $variation_id); } private function queue_notify($product_id, $variation_id) { if (function_exists('as_enqueue_async_action')) { as_enqueue_async_action('aieo/waitlist_notify', array('product_id' => $product_id, 'variation_id' => $variation_id), 'aieo'); } else { $this->run_notify($product_id, $variation_id); } } public function as_notify($product_id = 0, $variation_id = 0, $after_id = 0) { $this->run_notify((int) $product_id, (int) $variation_id, (int) $after_id); } private function run_notify($product_id, $variation_id, $after_id = 0) { global $wpdb; $table = self::get_table_name(); $lim = self::NOTIFY_BATCH + 1; if ($variation_id > 0) { $subs = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$table} WHERE variation_id = %d AND status = 'subscribed' AND notified_at IS NULL AND id > %d ORDER BY id ASC LIMIT %d", $variation_id, $after_id, $lim )); } else { $subs = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$table} WHERE product_id = %d AND status = 'subscribed' AND notified_at IS NULL AND id > %d ORDER BY id ASC LIMIT %d", $product_id, $after_id, $lim )); } $more = count($subs) > self::NOTIFY_BATCH; if ($more) { array_pop($subs); } if (empty($subs)) { return; } $last = end($subs); $cursor = (int) $last->id; reset($subs); $this->notify_rows($subs); if ($more && function_exists('as_enqueue_async_action')) { as_enqueue_async_action('aieo/waitlist_notify', array('product_id' => $product_id, 'variation_id' => $variation_id, 'after_id' => $cursor), 'aieo'); } } private function notify_rows($subs) { if (empty($subs)) { return; } global $wpdb; $table = self::get_table_name(); $settings = $this->get_settings(); foreach ($subs as $sub) { $waited_pid = ((int) $sub->variation_id > 0) ? (int) $sub->variation_id : (int) $sub->product_id; $waited = wc_get_product($waited_pid); if (!$waited || 'instock' !== $waited->get_stock_status()) { continue; } $product = wc_get_product((int) $sub->product_id); if (!$product) { continue; } $claimed = (int) $wpdb->query($wpdb->prepare( "UPDATE {$table} SET status = 'notified', notified_at = %s WHERE id = %d AND status = 'subscribed' AND notified_at IS NULL", current_time('mysql', true), (int) $sub->id )); if ($claimed < 1) { continue; } if (!$this->send_email($sub->email, $product, $settings)) { $wpdb->query($wpdb->prepare( "UPDATE {$table} SET status = 'subscribed', notified_at = NULL WHERE id = %d", (int) $sub->id )); } } } private function send_email($email, $product, $settings) { $name = $product->get_name(); $url = $product->get_permalink(); $subject = str_replace('{product_name}', $name, $settings['email_subject']); $message = str_replace( array('{product_name}', '{product_url}'), array($name, $url), $settings['email_message'] ); $message .= "\n\n" . sprintf(__('View product: %s', 'ai-eshop-optimizer'), $url); return (bool) wp_mail($email, $subject, $message, array('Content-Type: text/plain; charset=UTF-8')); } }