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 '';
}
}