register_display(); } } add_action('admin_init', array($this, 'maybe_migrate_exit_intent')); add_action('admin_post_' . self::DELETE_ACTION, array($this, 'handle_delete')); if (!class_exists('AIEO_Post_Duplicator') && defined('AIEO_DIR') && file_exists(AIEO_DIR . 'includes/class-aieo-post-duplicator.php')) { require_once AIEO_DIR . 'includes/class-aieo-post-duplicator.php'; } if (class_exists('AIEO_Post_Duplicator')) { AIEO_Post_Duplicator::register(); } } public static function delete_url($post_id) { return wp_nonce_url( add_query_arg( array('action' => self::DELETE_ACTION, 'popup' => (int) $post_id), admin_url('admin-post.php') ), self::DELETE_ACTION . '_' . (int) $post_id ); } public function handle_delete() { $pid = isset($_GET['popup']) ? (int) $_GET['popup'] : 0; check_admin_referer(self::DELETE_ACTION . '_' . $pid); $cap = defined('AIEO_CAPABILITY') ? AIEO_CAPABILITY : 'manage_options'; if ($pid <= 0 || !current_user_can($cap) || !current_user_can('delete_post', $pid)) { wp_die(esc_html__('You are not allowed to delete this popup.', 'ai-eshop-optimizer'), '', array('response' => 403)); } if (get_post_type($pid) !== 'aieo_popup') { wp_die(esc_html__('That is not a popup.', 'ai-eshop-optimizer'), '', array('response' => 400)); } $title = get_the_title($pid); $ok = (bool) wp_trash_post($pid); $this->flush_index(); if (class_exists('AIEO_Popup_CPT')) { AIEO_Popup_CPT::purge_caches(); } wp_safe_redirect(add_query_arg( array('aieo_popup_deleted' => $ok ? 1 : 0, 'aieo_popup_title' => rawurlencode($title)), $this->get_url() )); exit; } public static function configured_coupon_codes() { if (!class_exists('AIEO_Popup_Display') && defined('AIEO_DIR') && file_exists(AIEO_DIR . 'includes/class-aieo-popup-display.php')) { require_once AIEO_DIR . 'includes/class-aieo-popup-display.php'; } return class_exists('AIEO_Popup_Display') ? AIEO_Popup_Display::configured_coupon_codes() : array(); } public function flush_index() { if (class_exists('AIEO_Popup_Display')) { AIEO_Popup_Display::instance()->flush_index(); } elseif (class_exists('AIEO_DMM_Cache')) { AIEO_DMM_Cache::bump_version(self::CACHE_DOMAIN); } } public function maybe_migrate_exit_intent() { if (get_option(self::MIGRATED_OPTION)) { return; } if (!current_user_can(defined('AIEO_CAPABILITY') ? AIEO_CAPABILITY : 'manage_options')) { return; } $this->migrate_exit_intent(); } public function migrate_exit_intent() { if (!add_option(self::MIGRATED_OPTION, 1, '', false)) { if (get_option(self::MIGRATED_OPTION)) { return 0; } update_option(self::MIGRATED_OPTION, 1, false); } $legacy = get_option(self::LEGACY_OPTION, null); if (!is_array($legacy)) { return 0; } if (!class_exists('AIEO_Popup_CPT')) { return 0; } $enabled = (($legacy['enabled'] ?? 'no') === 'yes'); $created = 0; $variants = array( 'default' => array( 'body' => (string) ($legacy['content'] ?? ''), 'title' => __('Exit intent (storefront)', 'ai-eshop-optimizer'), 'surface' => 'exit_intent_default', 'delay' => (int) ($legacy['minimum_time'] ?? 120), 'coupon' => (string) ($legacy['coupon_default'] ?? ''), 'pages' => (($legacy['only_checkout'] ?? 'no') === 'yes') ? array('checkout') : array('all'), 'order' => 20, ), 'checkout' => array( 'body' => (string) ($legacy['checkout_content'] ?? ''), 'title' => __('Exit intent (checkout)', 'ai-eshop-optimizer'), 'surface' => 'exit_intent_checkout', 'delay' => (int) ($legacy['checkout_minimum_time'] ?? 120), 'coupon' => (string) ($legacy['coupon_checkout'] ?? ''), 'pages' => array('checkout'), 'order' => 10, ), ); foreach ($variants as $v) { if (trim($v['body']) === '') { continue; } $content = "\n" . $v['body'] . "\n"; $post_id = wp_insert_post(array( 'post_type' => 'aieo_popup', 'post_status' => $enabled ? 'publish' : 'draft', 'post_title' => $v['title'], 'post_content' => $content, 'menu_order' => $v['order'], ), true); if (is_wp_error($post_id) || !$post_id) { continue; } update_post_meta($post_id, AIEO_Popup_CPT::META_SURFACE, $v['surface']); update_post_meta($post_id, AIEO_Popup_CPT::META_TRIGGER, 'exit'); update_post_meta($post_id, AIEO_Popup_CPT::META_DELAY_MS, max(0, $v['delay']) * 1000); update_post_meta($post_id, AIEO_Popup_CPT::META_FREQ, 'session'); update_post_meta($post_id, AIEO_Popup_CPT::META_PAGES, $v['pages']); update_post_meta($post_id, AIEO_Popup_CPT::META_COUPON, $v['coupon']); update_post_meta($post_id, AIEO_Popup_CPT::META_DATA_DELAY, (($legacy['data_driven_delay'] ?? 'no') === 'yes') ? 'yes' : 'no'); update_post_meta($post_id, AIEO_Popup_CPT::META_VISITOR, (($legacy['anonymous_only'] ?? 'no') === 'yes') ? 'guest' : 'all'); $created++; } $this->flush_index(); return $created; } public function render_subtab() { $this->render_list_card(); $this->render_report_card(); } private function render_list_card() { $posts = post_type_exists('aieo_popup') ? get_posts(array( 'post_type' => 'aieo_popup', 'post_status' => array('publish', 'draft', 'pending', 'future', 'private'), 'numberposts' => 100, 'orderby' => array('menu_order' => 'ASC', 'ID' => 'DESC'), )) : array(); $new_url = admin_url('post-new.php?post_type=aieo_popup'); $trigger_labels = array( 'load' => __('On load', 'ai-eshop-optimizer'), 'delay' => __('After a delay', 'ai-eshop-optimizer'), 'scroll' => __('On scroll', 'ai-eshop-optimizer'), 'exit' => __('Exit intent', 'ai-eshop-optimizer'), ); $state_labels = array( 'before' => __('Scheduled', 'ai-eshop-optimizer'), 'live' => __('Live', 'ai-eshop-optimizer'), 'ended' => __('Finished', 'ai-eshop-optimizer'), ); $page_labels = array( 'all' => __('Everywhere', 'ai-eshop-optimizer'), 'front' => __('Front page', 'ai-eshop-optimizer'), 'blog' => __('Blog', 'ai-eshop-optimizer'), 'shop' => __('Shop', 'ai-eshop-optimizer'), 'product' => __('Product pages', 'ai-eshop-optimizer'), 'product_cat' => __('Product categories', 'ai-eshop-optimizer'), 'cart' => __('Cart', 'ai-eshop-optimizer'), 'checkout' => __('Checkout', 'ai-eshop-optimizer'), 'account' => __('My account', 'ai-eshop-optimizer'), 'ids' => __('Specific IDs', 'ai-eshop-optimizer'), ); $device_labels = array( 'all' => __('Any device', 'ai-eshop-optimizer'), 'desktop' => __('Desktop only', 'ai-eshop-optimizer'), 'mobile' => __('Mobile only', 'ai-eshop-optimizer'), ); ?> 'popups-overview', 'title' => __('Your popups', 'ai-eshop-optimizer'), 'description' => __('Design a popup with the block editor, then choose when it runs, where it appears and who sees it. Each popup is a post, so it translates like any other content.', 'ai-eshop-optimizer'), 'tools' => '' . esc_html__('Add popup', 'ai-eshop-optimizer') . '', 'footnote' => empty($posts) ? '' : __('When more than one popup qualifies for the same page view, the one with the lowest order (Page Attributes → Order) wins. Only one popup shows per page view.', 'ai-eshop-optimizer'), 'flush' => true, )); echo $open; ?> __('No popups yet. Add one, and start from the “Popup — split media” pattern for a layout with a picture beside the text.', 'ai-eshop-optimizer'))); ?> ID; $state = AIEO_Popup_CPT::schedule_state($pid); $start = AIEO_Popup_CPT::fmt_dt(AIEO_Popup_CPT::get($pid, AIEO_Popup_CPT::META_START)); $end = AIEO_Popup_CPT::fmt_dt(AIEO_Popup_CPT::get($pid, AIEO_Popup_CPT::META_END)); $trig = (string) AIEO_Popup_CPT::get($pid, AIEO_Popup_CPT::META_TRIGGER); $pages = (array) AIEO_Popup_CPT::get($pid, AIEO_Popup_CPT::META_PAGES); $device = (string) AIEO_Popup_CPT::get($pid, AIEO_Popup_CPT::META_DEVICE); $live = ('publish' === $p->post_status && 'live' === $state); $title = trim(get_the_title($pid)); if ('' === $title) { $title = __('(no title)', 'ai-eshop-optimizer'); } $status_labels = array( 'draft' => __('Draft', 'ai-eshop-optimizer'), 'pending' => __('Pending review', 'ai-eshop-optimizer'), 'future' => __('Scheduled by WordPress', 'ai-eshop-optimizer'), 'private' => __('Private — not shown to visitors', 'ai-eshop-optimizer'), ); $status = ('publish' !== $p->post_status) ? (isset($status_labels[$p->post_status]) ? $status_labels[$p->post_status] : $p->post_status) : ($state_labels[$state] ?? $state); $names = array(); foreach ($pages as $tok) { $names[] = isset($page_labels[$tok]) ? $page_labels[$tok] : $tok; } $popup_rows[] = array( 'pid' => $pid, 'title' => $title, 'edit_url' => (string) get_edit_post_link($pid), 'view_url' => (string) get_permalink($pid), 'dupe_url' => class_exists('AIEO_Post_Duplicator') ? (string) AIEO_Post_Duplicator::url($pid) : '', 'del_url' => (string) self::delete_url($pid), 'status' => $status, 'live' => $live, 'test_mode' => 'yes' === AIEO_Popup_CPT::get($pid, AIEO_Popup_CPT::META_TEST) ? __('TEST MODE', 'ai-eshop-optimizer') : '', 'window' => ('' === $start && '' === $end) ? __('Always', 'ai-eshop-optimizer') : trim(('' !== $start ? $start : '…') . ' → ' . ('' !== $end ? $end : '…')), 'trigger' => $trigger_labels[$trig] ?? $trig, 'where' => implode(', ', $names), 'device' => $device_labels[$device] ?? $device, ); } $in_test = array(); foreach ($posts as $p) { if (AIEO_Popup_CPT::get($p->ID, AIEO_Popup_CPT::META_TEST) === 'yes') { $in_test[] = get_the_title($p->ID); } } if ($in_test) { $aieo_pop_test = \Rooster\Admin\Ui::notice( esc_html(sprintf( __('%s is showing on every page, every time, to everyone who visits the shop — and is recording no statistics. Switch it off when you have finished designing.', 'ai-eshop-optimizer'), implode(', ', $in_test) )), 'warning', array('title' => __('Test mode is on.', 'ai-eshop-optimizer'), 'inline' => true) ); echo $aieo_pop_test; } AIEO_Table::card(array( 'wrap' => false, 'page_id' => $this->get_id(), 'row_key' => 'pid', 'rows' => $popup_rows, 'empty' => __('No popups yet. Add one, and start from the “Popup — split media” pattern for a layout with a picture beside the text.', 'ai-eshop-optimizer'), 'row_class' => static function ($r) { return empty($r['live']) ? 'aieo-t-tone--muted' : ''; }, 'columns' => array( array('key' => 'title', 'label' => __('Popup', 'ai-eshop-optimizer'), 'type' => 'link', 'format' => array('href' => 'edit_url')), array('key' => 'status', 'label' => __('Status', 'ai-eshop-optimizer'), 'type' => 'badge', 'subline' => 'test_mode'), array('key' => 'window', 'label' => __('Active period', 'ai-eshop-optimizer'), 'type' => 'text', 'nowrap' => true), array('key' => 'trigger', 'label' => __('Trigger', 'ai-eshop-optimizer'), 'type' => 'text'), array('key' => 'where', 'label' => __('Where', 'ai-eshop-optimizer'), 'type' => 'text'), array('key' => 'device', 'label' => __('Devices', 'ai-eshop-optimizer'), 'type' => 'text'), ), 'actions' => array( array('id' => 'edit', 'label' => __('Edit', 'ai-eshop-optimizer'), 'href' => static function ($r) { return $r['edit_url']; }), array('id' => 'preview', 'label' => __('Preview', 'ai-eshop-optimizer'), 'href' => static function ($r) { return $r['view_url']; }), array( 'id' => 'duplicate', 'label' => __('Duplicate', 'ai-eshop-optimizer'), 'visible' => static function ($r) { return '' !== $r['dupe_url']; }, 'href' => static function ($r) { return $r['dupe_url']; }, ), array( 'id' => 'delete', 'label' => __('Delete', 'ai-eshop-optimizer'), 'destructive' => true, 'confirm_level' => 'reset', 'href' => static function ($r) { return $r['del_url']; }, 'confirm' => static function ($r) { return sprintf( __('Move “%s” to the trash? It stops showing immediately, and you can restore it from the trash.', 'ai-eshop-optimizer'), $r['title'] ); }, ), ), )); ?> prefix . 'aieo_reco_events'; $attr_tbl = $wpdb->prefix . 'aieo_dmm_offer_attributions'; $window_days = 30; $since = gmdate('Y-m-d H:i:s', time() - $window_days * DAY_IN_SECONDS); $labels = array(); $coupon_to_surf = array(); $posts = post_type_exists('aieo_popup') ? get_posts(array('post_type' => 'aieo_popup', 'post_status' => array('publish', 'draft', 'pending', 'future', 'private'), 'numberposts' => 100)) : array(); foreach ($posts as $p) { $surf = AIEO_Popup_CPT::surface_tag($p->ID); $labels[$surf] = get_the_title($p->ID); $code = strtolower(trim((string) get_post_meta($p->ID, AIEO_Popup_CPT::META_COUPON, true))); if ($code !== '') { $coupon_to_surf[$code] = $surf; } } if (empty($labels)) { return; } $by_surface = array(); foreach (array_keys($labels) as $surf) { $by_surface[$surf] = array('impressions' => 0, 'dismissals' => 0, 'redemptions' => 0, 'cost' => 0.0); } $ph = implode(',', array_fill(0, count($labels), '%s')); $params = array_merge(array_keys($labels), array($since)); $rows = $wpdb->get_results($wpdb->prepare( "SELECT surface, event_type, COUNT(*) AS c FROM {$reco_tbl} WHERE surface IN ({$ph}) AND event_type IN ('ei_impression','ei_dismiss') AND ts >= %s GROUP BY surface, event_type", $params ), ARRAY_A); if (is_array($rows)) { foreach ($rows as $r) { $surf = (string) $r['surface']; if (!isset($by_surface[$surf])) { continue; } $key = $r['event_type'] === 'ei_impression' ? 'impressions' : 'dismissals'; $by_surface[$surf][$key] = (int) $r['c']; } } if (!empty($coupon_to_surf)) { $cph = implode(',', array_fill(0, count($coupon_to_surf), '%s')); $cargs = array_merge(array_keys($coupon_to_surf), array($since)); $crows = $wpdb->get_results($wpdb->prepare( "SELECT LOWER(offer_label) AS code, COUNT(*) AS orders, SUM(actual_cost) AS cost FROM {$attr_tbl} WHERE offer_type = '" . self::OFFER_TYPE . "' AND LOWER(offer_label) IN ({$cph}) AND fired_at >= %s GROUP BY LOWER(offer_label)", $cargs ), ARRAY_A); if (is_array($crows)) { foreach ($crows as $r) { $surf = $coupon_to_surf[(string) $r['code']] ?? ''; if ($surf === '' || !isset($by_surface[$surf])) { continue; } $by_surface[$surf]['redemptions'] += (int) $r['orders']; $by_surface[$surf]['cost'] += (float) $r['cost']; } } } $fmt_money = static function ($v) { return function_exists('wc_price') ? wp_strip_all_tags(wc_price((float) $v)) : number_format((float) $v, 2); }; $rate = static function ($num, $denom) { $denom = (int) $denom; return $denom > 0 ? round(($num / $denom) * 100, 1) . '%' : '—'; }; $perf = array(); $totals = array( 'impressions' => 0, 'dismissals' => 0, 'redemptions' => 0, 'cost' => 0.0 ); foreach ($by_surface as $surf => $row) { $totals['impressions'] += $row['impressions']; $totals['dismissals'] += $row['dismissals']; $totals['redemptions'] += $row['redemptions']; $totals['cost'] += $row['cost']; $perf[] = array( 'surface' => (string) $surf, 'label' => $labels[$surf], 'impressions' => (int) $row['impressions'], 'dismissals' => (int) $row['dismissals'], 'redemptions' => (int) $row['redemptions'], 'rate' => (int) $row['impressions'] > 0 ? round( ( $row['redemptions'] / $row['impressions'] ) * 100, 1 ) : null, 'cost' => (float) $row['cost'], ); } $shell = AIEO_Table::card_shell(array( 'anchor' => 'popups-performance-last-30-days', 'flush' => true, 'title' => __('Popup performance (30 days)', 'ai-eshop-optimizer'), 'description' => __('Impressions and dismissals come from the shared reco-events table; redemptions and cost from the shared offer-attributions table — the same plumbing the rest of the plugin uses, with no extra schema. Events are only recorded when the visitor has accepted analytics cookies, so these are consented counts.', 'ai-eshop-optimizer'), )); echo $shell[0]; AIEO_Table::card(array( 'wrap' => false, 'page_id' => $this->get_id(), 'row_key' => 'surface', 'density' => 'compact', 'rows' => $perf, 'empty' => __('No popup events in this window.', 'ai-eshop-optimizer'), 'totals_at' => 'foot', 'totals_label' => __('Total', 'ai-eshop-optimizer'), 'totals' => array( 'impressions' => $totals['impressions'], 'dismissals' => $totals['dismissals'], 'redemptions' => $totals['redemptions'], 'rate' => $totals['impressions'] > 0 ? round( ( $totals['redemptions'] / $totals['impressions'] ) * 100, 1 ) : null, 'cost' => $totals['cost'], ), 'columns' => array( array('key' => 'label', 'label' => __('Popup', 'ai-eshop-optimizer'), 'type' => 'text'), array('key' => 'impressions', 'label' => __('Impressions', 'ai-eshop-optimizer'), 'type' => 'integer'), array('key' => 'dismissals', 'label' => __('Dismissals', 'ai-eshop-optimizer'), 'type' => 'integer'), array('key' => 'redemptions', 'label' => __('Redemptions', 'ai-eshop-optimizer'), 'type' => 'integer'), array('key' => 'rate', 'label' => __('Conv. rate', 'ai-eshop-optimizer'), 'type' => 'percent', 'format' => array('precision' => 1), 'total' => array('ratio' => array('redemptions', 'impressions'), 'percent' => true)), array('key' => 'cost', 'label' => __('Total cost', 'ai-eshop-optimizer'), 'type' => 'money'), ), )); echo \Rooster\Admin\Guidance::text(__('“Conv. rate” is redemptions ÷ impressions. Cost is the discount each attributed order absorbed. Popups with no coupon show no redemptions by design — measure those on dismissal rate instead.', 'ai-eshop-optimizer')); $this->render_efficacy_timeseries($window_days, $since, array_keys($labels)); echo $shell[1]; } private function render_efficacy_timeseries($window_days, $since, array $surfaces) { global $wpdb; if (empty($surfaces)) { return; } $reco_tbl = $wpdb->prefix . 'aieo_reco_events'; $attr_tbl = $wpdb->prefix . 'aieo_dmm_offer_attributions'; $buckets = array(); for ($i = $window_days - 1; $i >= 0; $i--) { $buckets[gmdate('Y-m-d', time() - $i * DAY_IN_SECONDS)] = array('impressions' => 0, 'redemptions' => 0); } $ph = implode(',', array_fill(0, count($surfaces), '%s')); $rows = $wpdb->get_results($wpdb->prepare( "SELECT DATE(ts) AS d, COUNT(*) AS n FROM {$reco_tbl} WHERE event_type = 'ei_impression' AND surface IN ({$ph}) AND ts >= %s GROUP BY DATE(ts)", array_merge($surfaces, array($since)) ), ARRAY_A); if (is_array($rows)) { foreach ($rows as $r) { $d = (string) $r['d']; if (isset($buckets[$d])) { $buckets[$d]['impressions'] = (int) $r['n']; } } } $rows = $wpdb->get_results($wpdb->prepare( "SELECT DATE(fired_at) AS d, COUNT(*) AS n FROM {$attr_tbl} WHERE offer_type = '" . self::OFFER_TYPE . "' AND fired_at >= %s GROUP BY DATE(fired_at)", $since ), ARRAY_A); if (is_array($rows)) { foreach ($rows as $r) { $d = (string) $r['d']; if (isset($buckets[$d])) { $buckets[$d]['redemptions'] = (int) $r['n']; } } } $rates = array(); foreach ($buckets as $b) { $impr = max(0, (int) $b['impressions']); $red = max(0, (int) $b['redemptions']); $rates[] = $impr > 0 ? min(100, ($red / $impr) * 100) : 0; } ?> 'popups-conv-rate-trend', 'icon' => 'chart-line', 'level' => 3, 'title' => __('Daily efficacy', 'ai-eshop-optimizer'), 'meta' => AIEO_DMM_UI::title_badge(sprintf(__('Last %d days', 'ai-eshop-optimizer'), (int) $window_days)), 'description' => __('One row per UTC day. Use it to spot creative fatigue and the lift from a copy edit.', 'ai-eshop-optimizer'), 'flush' => true, 'tools' => $aieo_pop_trend, )); echo $open; ?> $b) { $impr = (int) $b['impressions']; $red = (int) $b['redemptions']; $days[] = array( 'date' => (string) $date, 'impressions' => $impr, 'redemptions' => $red, 'rate' => $impr > 0 ? round(($red / $impr) * 100, 1) : null, ); } AIEO_Table::card(array( 'wrap' => false, 'page_id' => $this->get_id(), 'row_key' => 'date', 'density' => 'compact', 'rows' => $days, 'empty' => __('No daily events in this window.', 'ai-eshop-optimizer'), 'columns' => array( array('key' => 'date', 'label' => __('Date', 'ai-eshop-optimizer'), 'type' => 'text', 'nowrap' => true), array('key' => 'impressions', 'label' => __('Impressions', 'ai-eshop-optimizer'), 'type' => 'integer'), array('key' => 'redemptions', 'label' => __('Redemptions', 'ai-eshop-optimizer'), 'type' => 'integer'), array('key' => 'rate', 'label' => __('Conv. rate', 'ai-eshop-optimizer'), 'type' => 'percent', 'format' => array('precision' => 1)), ), )); ?> '; } $max = 0.0; foreach ($values as $v) { if ($v > $max) { $max = (float) $v; } } if ($max <= 0) { $max = 1.0; } $pad = 2; $iw = max(1, $w - 2 * $pad); $ih = max(1, $h - 2 * $pad); $step = $iw / ($n - 1); $points = array(); foreach ($values as $i => $v) { $x = $pad + $i * $step; $y = $pad + $ih - (((float) $v / $max) * $ih); $points[] = round($x, 1) . ',' . round($y, 1); } return '' . '' . ''; } }