[label, render (callable), cap]. */ public static function views() { if (self::$views !== null) { return self::$views; } $admin = AIFLT_PLUGIN_PATH . 'includes/admin/'; $template = static function ($file) use ($admin) { return static function () use ($admin, $file) { include $admin . $file; }; }; $views = [ 'leak-scan' => [ 'slug' => 'ai_flash_tune', 'icon' => 'droplet', 'title' => __('Leak Scan', 'ai-flash-tune'), 'group' => 'main', 'tabs' => ['overview' => ['label' => '', 'render' => $template('leak-scan.php')]], ], 'funnel' => [ 'slug' => 'aiflt-funnel', 'icon' => 'funnel', 'title' => __('Funnel', 'ai-flash-tune'), 'group' => 'main', 'tabs' => ['overview' => ['label' => '', 'render' => $template('funnel-analysis.php')]], ], 'products' => [ 'slug' => 'aiflt-products', 'icon' => 'package', 'title' => __('Products', 'ai-flash-tune'), 'group' => 'main', 'tabs' => ['performance' => ['label' => '', 'render' => $template('products.php')]], ], 'product-detail' => [ 'slug' => 'aiflt-product-detail', 'icon' => 'package', 'title' => __('Product', 'ai-flash-tune'), // The product's own name as the page title. 'title_cb' => static function () { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only. $id = isset($_GET['product_id']) ? absint($_GET['product_id']) : 0; return $id && get_post_type($id) === 'product' ? get_the_title($id) : ''; }, 'group' => 'hidden', 'parent' => 'products', 'tabs' => ['overview' => ['label' => '', 'render' => $template('product-detail.php')]], ], 'carts' => [ 'slug' => 'aiflt-abandoned-carts', 'icon' => 'cart', 'title' => __('Carts', 'ai-flash-tune'), 'group' => 'main', 'tabs' => ['carts' => ['label' => '', 'render' => $template('abandoned-carts.php')]], ], 'checkout' => [ 'slug' => 'aiflt-checkout', 'icon' => 'card', 'title' => __('Checkout', 'ai-flash-tune'), 'group' => 'main', 'tabs' => ['fields' => ['label' => '', 'render' => $template('checkout.php')]], ], 'reports' => [ 'slug' => 'aiflt-reports', 'icon' => 'chart', 'title' => __('Reports', 'ai-flash-tune'), 'group' => 'main', // AI Flash Tune Pro replaces the Summary render with the store summary. // Without Pro, Reports opens on what works (Client reports) and the // Summary preview comes last. The page tips are about Client reports. 'tabs' => aiflt_is_pro_active() ? [ 'summary' => ['label' => __('Summary', 'ai-flash-tune'), 'render' => self::locked('summary'), 'intro' => false], 'client' => ['label' => __('Client reports', 'ai-flash-tune'), 'render' => $template('growth-tools.php')], 'ai-log' => ['label' => __('AI activity', 'ai-flash-tune'), 'render' => $template('ai-usage-reports.php'), 'intro' => false], ] : [ 'client' => ['label' => __('Client reports', 'ai-flash-tune'), 'render' => $template('growth-tools.php')], 'ai-log' => ['label' => __('AI activity', 'ai-flash-tune'), 'render' => $template('ai-usage-reports.php'), 'intro' => false], 'summary' => ['label' => __('Summary', 'ai-flash-tune'), 'render' => self::locked('summary'), 'intro' => false], ], ], 'import' => [ 'slug' => 'aiflt-import', 'icon' => 'download', 'title' => __('Import', 'ai-flash-tune'), 'group' => 'footer', 'cap' => 'manage_woocommerce', // Another cart plugin's data found here and not brought over yet. 'badge' => static function () { return class_exists('AIFLT_Cart_Import') ? AIFLT_Cart_Import::waiting_count() : 0; }, 'tabs' => ['overview' => ['label' => '', 'cap' => 'manage_woocommerce', 'render' => $template('import.php')]], ], 'settings' => [ 'slug' => 'aiflt_settings', 'icon' => 'sliders', 'title' => __('Settings', 'ai-flash-tune'), 'group' => 'footer', 'tabs' => [ 'general' => ['label' => __('General', 'ai-flash-tune'), 'render' => $template('settings.php')], 'privacy' => ['label' => __('Privacy requests', 'ai-flash-tune'), 'render' => $template('privacy-tools.php')], 'system' => ['label' => __('System status', 'ai-flash-tune'), 'render' => $template('help.php')], ], ], 'licence' => [ 'slug' => 'aiflt-licence', 'icon' => 'key', 'title' => __('Licence', 'ai-flash-tune'), 'group' => 'footer', 'status' => static function () { return aiflt_is_pro_active() ? ['Active', 'good'] : ['Free', 'neutral']; }, 'tabs' => ['overview' => ['label' => '', 'render' => $template('pro.php')]], ], // Pro pages are always listed; without a licence they explain what they do. // AI Flash Tune Pro replaces their render when it is licensed. 'action-plan' => [ 'slug' => 'aiflt-action-plan', 'icon' => 'list-checks', 'title' => __('Action Plan', 'ai-flash-tune'), 'group' => 'pro', 'pro' => true, 'tabs' => ['overview' => ['label' => '', 'render' => self::locked('action-plan')]], ], 'alerts' => [ 'slug' => 'aiflt-alerts', 'icon' => 'bell', 'title' => __('Alerts', 'ai-flash-tune'), 'group' => 'pro', 'pro' => true, 'tabs' => ['overview' => ['label' => '', 'render' => self::locked('alerts')]], ], 'help' => [ 'slug' => 'aiflt-help', 'icon' => 'help', 'title' => __('Help', 'ai-flash-tune'), 'group' => 'footer', 'tabs' => ['overview' => ['label' => '', 'render' => $template('help-guide.php')]], ], ]; /** * Add pages or tabs. AI Flash Tune Pro uses this for its modules. */ // Footer order: Settings, Licence, Help. $help = $views['help']; unset($views['help']); $views['help'] = $help; $views = apply_filters('aiflt_admin_views', $views); foreach ($views as $key => &$view) { $view['key'] = $key; $view['cap'] = $view['cap'] ?? 'manage_options'; $view['group'] = $view['group'] ?? 'main'; } unset($view); self::$views = $views; return $views; } /** * Plain-language guidance for each page, used by the sidebar and the page * header so every screen answers "what is this?" and "what do I do here?". * * section Sidebar group: start|find|win|report (anything else: "More"). * hint One short line under the sidebar link. * subtitle One sentence under the page title. * steps Up to three short "do this here" steps for the page intro. * * AI Flash Tune Pro (or anyone) can add or change entries with the * `aiflt_admin_guide` filter, keyed by view name. * * @return array */ public static function guide() { static $guide = null; if ($guide !== null) { return $guide; } $guide = [ 'leak-scan' => [ 'section' => 'start', 'hint' => __('Your store at a glance', 'ai-flash-tune'), 'subtitle' => __('Where your store loses the most money right now, and the one thing to fix first.', 'ai-flash-tune'), 'steps' => [ __('Look at the "Biggest leak" card', 'ai-flash-tune'), __('Press "Do it" on a quick fix', 'ai-flash-tune'), __('Work down the "Do next" list', 'ai-flash-tune'), ], ], 'funnel' => [ 'section' => 'find', 'hint' => __('Where shoppers stop', 'ai-flash-tune'), 'subtitle' => __('Follow shoppers from first visit to order and see the step where most of them give up.', 'ai-flash-tune'), 'steps' => [ __('Pick a period at the top', 'ai-flash-tune'), __('Find the step with the lowest "kept" figure', 'ai-flash-tune'), __('Ask AI what to check first', 'ai-flash-tune'), ], ], 'products' => [ 'section' => 'find', 'hint' => __('Which products sell', 'ai-flash-tune'), 'subtitle' => __('Every product compared with your store average, so you know which pages to improve.', 'ai-flash-tune'), 'steps' => [ __('Filter by "Leaking"', 'ai-flash-tune'), __('Open a product', 'ai-flash-tune'), __('Fix what its page check points out', 'ai-flash-tune'), ], ], 'product-detail' => [ 'section' => 'find', 'subtitle' => __('How this one product turns visits into orders, and what may be getting in the way.', 'ai-flash-tune'), 'steps' => [ __('Compare its conversion with the store average', 'ai-flash-tune'), __('Check the frustration signals', 'ai-flash-tune'), __('Edit the product page', 'ai-flash-tune'), ], ], 'checkout' => [ 'section' => 'find', 'hint' => __('Shorter, safer checkout', 'ai-flash-tune'), 'subtitle' => __('Fewer fields means fewer people giving up. Each quick fix is one click and can be undone.', 'ai-flash-tune'), 'steps' => [ __('See how many fields shoppers fill in', 'ai-flash-tune'), __('Apply a quick fix', 'ai-flash-tune'), __('Come back in 14 days for the result', 'ai-flash-tune'), ], ], 'carts' => [ 'section' => 'win', 'hint' => __('Win back abandoned carts', 'ai-flash-tune'), 'subtitle' => __('Carts shoppers left behind, and the automatic emails that bring them back.', 'ai-flash-tune'), 'steps' => [ __('Turn on automatic reminders', 'ai-flash-tune'), __('Send yourself a test email', 'ai-flash-tune'), __('Open a cart\'s Details to follow up', 'ai-flash-tune'), ], ], 'reports' => [ 'section' => 'report', 'hint' => __('Share and look back', 'ai-flash-tune'), 'subtitle' => __('Reports for you or your clients, every change made to the store, and AI activity.', 'ai-flash-tune'), 'steps' => [ __('Download a report', 'ai-flash-tune'), __('Turn on the weekly email', 'ai-flash-tune'), __('Undo any change from the history', 'ai-flash-tune'), ], ], 'action-plan' => [ 'hint' => __('One ranked to-do list', 'ai-flash-tune'), 'subtitle' => __('All your store\'s numbers read together and turned into a short, ranked list of safe changes.', 'ai-flash-tune'), ], 'alerts' => [ 'hint' => __('Know when sales stop', 'ai-flash-tune'), 'subtitle' => __('An email the moment orders, checkout, payments or reminders stop working.', 'ai-flash-tune'), ], 'import' => [ 'subtitle' => __('Move carts and unsubscribed addresses over from another cart plugin, without losing a sale.', 'ai-flash-tune'), 'steps' => [ __('Import while the old plugin is on', 'ai-flash-tune'), __('Turn on reminders on the Carts page', 'ai-flash-tune'), __('Deactivate the old plugin and import again', 'ai-flash-tune'), ], ], 'settings' => [ 'subtitle' => __('Tracking, privacy and the optional AI provider. Nothing changes on your store until you save.', 'ai-flash-tune'), ], 'licence' => [ 'subtitle' => __('Your plan, and what Pro adds to the pages you already use.', 'ai-flash-tune'), ], 'help' => [ 'subtitle' => __('How AI Flash Tune works, how to test tracking, and where to get help.', 'ai-flash-tune'), ], ]; $guide = (array) apply_filters('aiflt_admin_guide', $guide); return $guide; } /** Guidance for one view (empty array when there is none). */ private static function guide_for($key) { $guide = self::guide(); return isset($guide[$key]) && is_array($guide[$key]) ? $guide[$key] : []; } /** * The getting-started checklist shown in the sidebar until everything is done. * * @return array list of [label, done (bool), url] */ private static function setup_items() { $state = class_exists('AIFLT_Leak_Scan') ? (string) (AIFLT_Leak_Scan::tracking_status()['state'] ?? 'waiting') : 'waiting'; $items = [ [__('Turn on tracking', 'ai-flash-tune'), $state !== 'off', aiflt_admin_url('settings')], [__('Record a first visit', 'ai-flash-tune'), in_array($state, ['working', 'quiet'], true), aiflt_admin_url('help')], ]; if (class_exists('AIFLT_Cart_Recovery')) { $items[] = [__('Turn on cart reminders', 'ai-flash-tune'), (bool) AIFLT_Cart_Recovery::is_enabled(), aiflt_admin_url('carts') . '#aiflt-reminders']; } $items[] = [__('Apply a first quick fix', 'ai-flash-tune'), self::has_applied_fix(), aiflt_admin_url('checkout') . '#aiflt-checkout-fixes']; /** * Getting-started checklist in the sidebar. Each item: [label, done, url]. */ return (array) apply_filters('aiflt_setup_items', $items); } /** Whether any checkout quick fix has been applied (cached per request). */ private static function has_applied_fix() { static $done = null; if ($done === null) { $done = false; if (class_exists('AIFLT_Fixes') && method_exists('AIFLT_Fixes', 'for_stage')) { foreach ((array) AIFLT_Fixes::for_stage('checkout', ['hide_company', 'phone_optional', 'guest_checkout', 'reassurance']) as $fix) { if (!empty($fix['done'])) { $done = true; break; } } } } return $done; } /** Sidebar card: getting-started progress. Hidden once every step is done. */ private static function render_setup() { $items = self::setup_items(); if (!$items) { return; } $done = count(array_filter($items, static function ($i) { return !empty($i[1]); })); if ($done === count($items)) { return; } $pct = (int) round($done / count($items) * 100); echo '
'; echo '

' . esc_html__('Getting started', 'ai-flash-tune') /* translators: 1: steps done, 2: steps in total */ . ' ' . esc_html(sprintf(__('%1$d of %2$d', 'ai-flash-tune'), $done, count($items))) . '

'; echo '
'; } /** * Sidebar card: which AI answers the plugin right now, with a link to change it. */ private static function render_ai_status() { $provider = (string) get_option('aiflt_ai_provider', 'local'); $names = ['openai' => 'OpenAI', 'claude' => 'Claude', 'zywrap' => 'Zywrap']; if (aiflt_is_pro_active()) { $state = 'is-on'; $label = __('AI included with Pro', 'ai-flash-tune'); } elseif (isset($names[$provider])) { $state = 'is-on'; /* translators: %s: AI provider name */ $label = sprintf(__('%s connected', 'ai-flash-tune'), $names[$provider]); } elseif ($provider !== 'local' && $provider !== '') { $state = 'is-on'; $label = __('AI provider connected', 'ai-flash-tune'); } else { $state = 'is-off'; $label = __('Built-in rules ยท add AI', 'ai-flash-tune'); } echo ''; echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Fixed SVG. echo '' . esc_html__('AI engine', 'ai-flash-tune') . '' . esc_html($label) . ''; } /** * "Do this here" strip under the page title. Dismissible per browser. */ private static function render_intro($view, $tab = []) { if (isset($tab['intro']) && $tab['intro'] === false) { return; } $g = self::guide_for($view['key']); $steps = array_slice(array_filter((array) ($g['steps'] ?? [])), 0, 3); if (!$steps) { return; } $id = 'aiflt-intro-' . $view['key']; echo '
'; echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Fixed SVG. echo '

' . esc_html__('Do this here', 'ai-flash-tune') . '

'; echo '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Fixed SVG. echo '
    '; foreach ($steps as $i => $step) { echo '
  1. ' . esc_html(number_format_i18n($i + 1)) . '' . esc_html($step) . '
  2. '; } echo '
'; // Remembered per browser; the tips come back if site data is cleared. echo ""; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Fixed script, id JSON-encoded. } public static function view_by_slug($slug) { foreach (self::views() as $view) { if ($view['slug'] === $slug) { return $view; } } return null; } public static function register_menus() { $views = self::views(); add_menu_page(__('AI Flash Tune', 'ai-flash-tune'), __('AI Flash Tune', 'ai-flash-tune'), 'manage_options', 'ai_flash_tune', [__CLASS__, 'render'], self::menu_icon(), 25); // Same order as the plugin's own sidebar: main pages, Pro, then Import, Settings, Licence, Help. $rank = ['main' => 0, 'pro' => 1, 'footer' => 2, 'hidden' => 3]; $position = array_flip(array_keys($views)); uasort($views, static function ($a, $b) use ($rank, $position) { $ga = $rank[$a['group']] ?? 0; $gb = $rank[$b['group']] ?? 0; return $ga === $gb ? $position[$a['key']] <=> $position[$b['key']] : $ga <=> $gb; }); foreach ($views as $view) { add_submenu_page( $view['group'] === 'hidden' ? 'ai_flash_tune_hide' : 'ai_flash_tune', $view['title'], $view['title'], $view['cap'], $view['slug'], [__CLASS__, 'render'] ); } // Kept for anything that still hooks the old menu action. do_action('aiflt_register_submenu_pages'); } /** * Pages that were merged keep working: their old addresses open the page * and tab that now holds them, with any other query arguments kept. */ public static function redirect_old_pages() { $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; $map = [ 'aiflt-setup-wizard' => ['leak-scan', ''], 'aiftas-segmentation' => ['funnel', ''], 'aiftpo-product-optimizer' => ['products', ''], 'aiftcr-recovery-center' => ['carts', ''], 'aiftcr-email-log' => ['carts', ''], 'aiftco-optimizer-dashboard' => ['checkout', ''], 'aiftrp-reports' => ['reports', 'summary'], 'aiflt-ai-usage-reports' => ['reports', 'ai-log'], 'aiflt-growth-tools' => ['reports', 'client'], 'aiflt-privacy' => ['settings', 'privacy'], 'aiflt-pro' => ['licence', ''], 'aiftac-action-center' => ['action-plan', ''], 'aiftal-alert-center' => ['alerts', ''], ]; // Tabs that became part of their page (2.0 layout) or moved. $tab = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : ''; $moved_tabs = [ 'aiflt-funnel' => ['compare' => ['funnel', '', ['compare' => 'device']]], 'aiflt-products' => ['optimizer' => ['products', '', []]], 'aiflt-abandoned-carts' => ['recovery' => ['carts', '', []]], 'aiflt-checkout' => ['optimizer' => ['checkout', '', []], 'fields' => ['checkout', '', []]], 'aiflt_settings' => ['pro' => ['licence', '', []]], ]; if (isset($moved_tabs[$page][$tab])) { [$view, $new_tab, $extra] = $moved_tabs[$page][$tab]; $args = []; foreach ((array) $_GET as $key => $value) { if (!in_array($key, ['page', 'tab'], true) && is_scalar($value)) { $args[sanitize_key($key)] = sanitize_text_field(wp_unslash($value)); } } wp_safe_redirect(add_query_arg($args + $extra, aiflt_admin_url($view, $new_tab))); exit; } // The page it lands on checks permissions itself. if (!isset($map[$page]) || !(current_user_can('manage_options') || current_user_can('manage_woocommerce'))) { return; } [$view, $tab] = $map[$page]; if ($page === 'aiftas-segmentation') { $_GET['compare'] = 'device'; } $args = []; foreach ((array) $_GET as $key => $value) { if ($key !== 'page' && is_scalar($value)) { $args[sanitize_key($key)] = sanitize_text_field(wp_unslash($value)); } } // The old Settings screen used its own "tab" values. if ($page !== 'aiflt_settings') { unset($args['tab']); } wp_safe_redirect(add_query_arg($args, aiflt_admin_url($view, $tab))); exit; } /** * Render callback for a Pro page or tab when Pro is not active. * * @param string $feature action-plan|alerts|summary * @return callable */ private static function locked($feature) { return static function () use ($feature) { $features = [ 'action-plan' => [ 'list-checks', __('One plan for the whole store', 'ai-flash-tune'), __('Action Plan reads your funnel, carts, checkout, products and page speed together and gives you a short, ranked list of safe changes, each with how to measure it and how to undo it.', 'ai-flash-tune'), ], 'alerts' => [ 'bell', __('Know the moment sales stop', 'ai-flash-tune'), __('Every hour, Pro compares orders, checkout, payments, visits and cart reminders with your store\'s own normal. You get one email when something breaks and one when it is back to normal.', 'ai-flash-tune'), ], 'summary' => [ 'chart', __('A store summary you can share', 'ai-flash-tune'), __('Summary writes a short report for any period, with the biggest opportunity, risks and next steps. Agencies get a white-label version for clients, and a CSV export.', 'ai-flash-tune'), ], ]; // The full preview page (hero, labelled example, how it works). $aiflt_feature = $feature; $preview = AIFLT_PLUGIN_PATH . 'includes/admin/pro-preview.php'; if (file_exists($preview)) { include $preview; return; } [$icon, $title, $text] = $features[$feature]; echo '
'; AIFLT_UI::empty_state( $icon, $title, $text, '' . esc_html__('See what Pro adds', 'ai-flash-tune') . '' ); echo '
'; }; } public static function body_class($classes) { return function_exists('aiflt_is_plugin_admin_page') && aiflt_is_plugin_admin_page() ? $classes . ' aiflt-b' : $classes; } public static function current_tab($view) { $tabs = self::visible_tabs($view); $requested = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : ''; return isset($tabs[$requested]) ? $requested : (string) array_key_first($tabs); } private static function visible_tabs($view) { return array_filter($view['tabs'], static function ($tab) { return current_user_can($tab['cap'] ?? 'manage_options'); }); } /** * Render the current page: frame, then the active tab's template. */ public static function render() { $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : 'ai_flash_tune'; $view = self::view_by_slug($page) ?: self::views()['leak-scan']; if (!current_user_can($view['cap'])) { wp_die(esc_html__('You do not have permission to access this page.', 'ai-flash-tune')); } $tab_key = self::current_tab($view); $tabs = self::visible_tabs($view); echo '
'; self::render_nav($view); echo '
'; self::render_header($view, $tabs, $tab_key); self::render_review_ask(); self::render_intro($view, $tabs[$tab_key] ?? []); if ($view['key'] === 'leak-scan') { self::render_setup(); } echo '
'; if (isset($tabs[$tab_key]) && is_callable($tabs[$tab_key]['render'])) { call_user_func($tabs[$tab_key]['render']); } echo '
'; } /** * One quiet line, after the first order a cart reminder won back, asking for * a review. Only on this plugin's pages, and gone once the user answers. */ private static function render_review_ask() { if (!get_option('aiflt_first_recovery') || get_user_meta(get_current_user_id(), 'aiflt_review_ask', true) || !current_user_can('manage_options')) { return; } $button = static function ($do, $label, $class, $new_tab) { return '
' . wp_nonce_field('aiflt_review_ask', '_wpnonce', true, false) . '' . '' . '
'; }; $html = '
' . AIFLT_UI::icon('sparkles') . '

' . esc_html__('A cart reminder won back an order for you. If AI Flash Tune helps your store, a short review on WordPress.org helps other store owners find it.', 'ai-flash-tune') . '

' . $button('rate', __('Leave a review', 'ai-flash-tune'), 'aiflt-button aiflt-button-sm', true) . $button('dismiss', __('No thanks', 'ai-flash-tune'), 'aiflt-button aiflt-button-quiet aiflt-button-sm', false) . '
'; echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built from escaped parts and a fixed SVG. } /** The review line is answered once, per user: "rated" or "dismissed". */ public static function handle_review_ask() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ai-flash-tune')); } check_admin_referer('aiflt_review_ask'); $rate = sanitize_key(wp_unslash($_POST['do'] ?? '')) === 'rate'; update_user_meta(get_current_user_id(), 'aiflt_review_ask', $rate ? 'rated' : 'dismissed'); if ($rate) { add_filter('allowed_redirect_hosts', static function ($hosts) { $hosts[] = 'wordpress.org'; return $hosts; }); wp_safe_redirect('https://wordpress.org/support/plugin/ai-flash-tune/reviews/#new-post'); exit; } wp_safe_redirect(wp_get_referer() ?: aiflt_admin_url('leak-scan')); exit; } private static function render_header($view, $tabs, $active) { $parent = !empty($view['parent']) ? (self::views()[$view['parent']] ?? null) : null; echo '
'; if ($parent) { echo '← ' . esc_html($parent['title']) . ''; } $title = isset($view['title_cb']) && is_callable($view['title_cb']) ? (string) call_user_func($view['title_cb']) : ''; echo '

' . esc_html($title !== '' ? $title : $view['title']) . '

'; $subtitle = (string) (self::guide_for($view['key'])['subtitle'] ?? ''); if ($subtitle !== '') { echo '

' . esc_html($subtitle) . '

'; } echo '
'; self::render_ai_status(); echo '
'; $labelled = array_filter($tabs, static function ($tab) { return $tab['label'] !== ''; }); if (count($labelled) > 1) { echo ''; } } /** * WordPress menu icon: the two linked loops of the AI Flash Tune mark, as a * one-colour SVG so WordPress can tint it for every admin colour scheme. */ private static function menu_icon() { $ring = static function ($cx, $cy, $r, $inner) { return sprintf('M%1$s %2$sa%3$s %3$s 0 1 0 %4$s 0a%3$s %3$s 0 1 0 -%4$s 0zM%5$s %2$sa%6$s %6$s 0 1 0 %7$s 0a%6$s %6$s 0 1 0 -%7$s 0z', $cx - $r, $cy, $r, 2 * $r, $cx - $inner, $inner, 2 * $inner); }; $svg = ''; return 'data:image/svg+xml;base64,' . base64_encode($svg); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- WordPress menu icons are SVG data URIs. } private static function render_nav($current) { $views = self::views(); $active_key = !empty($current['parent']) ? $current['parent'] : $current['key']; // Same flat list as the WordPress menu: main pages, Pro, then the footer. $groups = [ 'main' => '', 'pro' => __('Pro', 'ai-flash-tune'), ]; echo ''; } private static function nav_link($item, $active_key) { $badge = isset($item['badge']) && is_callable($item['badge']) ? (int) call_user_func($item['badge']) : 0; $tail = $badge > 0 ? '' . esc_html(number_format_i18n($badge)) . '' : ''; if (!empty($item['pro']) && !aiflt_is_pro_active()) { $tail = '' . AIFLT_UI::icon('lock') . ''; } if (isset($item['status']) && is_callable($item['status'])) { [$status_text, $status_tone] = call_user_func($item['status']); $tail = AIFLT_UI::badge($status_text === 'Active' ? __('Active', 'ai-flash-tune') : __('Free', 'ai-flash-tune'), $status_tone); } // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- $tail is a badge escaped inside AIFLT_UI::badge() or a fixed SVG; the icon is fixed SVG. printf( '%6$s%4$s%5$s', esc_url(aiflt_admin_url($item['key'])), $item['key'] === $active_key ? ' is-active' : '', $item['key'] === $active_key ? ' aria-current="page"' : '', esc_html($item['title']), $tail, AIFLT_UI::icon($item['icon'] ?? 'chevron-right', 'aiflt-nav-icon') ); // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped } } /** * Admin URL of a page (and tab) by its internal view name. * * @param string $view e.g. 'checkout' * @param string $tab optional tab key * @return string */ function aiflt_admin_url($view, $tab = '') { $views = AIFLT_Admin_UI::views(); $slug = isset($views[$view]) ? $views[$view]['slug'] : 'ai_flash_tune'; $args = ['page' => $slug]; if ($tab !== '') { $args['tab'] = $tab; } return add_query_arg($args, admin_url('admin.php')); }