'), 25 ); // Default submenu = same slug so the parent menu highlights when active add_submenu_page( 'allaccessible', __('Dashboard', 'allaccessible'), __('Dashboard', 'allaccessible'), 'manage_options', 'allaccessible', array($this, 'render_settings_page') ); } public function register_account_page() { add_submenu_page( 'allaccessible', __('Account & Settings', 'allaccessible'), __('Account', 'allaccessible'), 'manage_options', 'allaccessible-account', array($this, 'render_account_page') ); } public function register_widget_page() { add_submenu_page( 'allaccessible', __('Widget Customizer', 'allaccessible'), __('Widget Customizer', 'allaccessible'), 'manage_options', 'allaccessible-widget', array($this, 'render_widget_page') ); } /** * Standalone Widget Customizer page. */ public function render_widget_page() { if (!get_option('aacb_wizard_completed') && !get_option('aacb_accountID')) { wp_redirect(admin_url('admin.php?page=allaccessible-wizard')); exit; } $account_id = get_option('aacb_accountID'); $has_account = !empty($account_id); $site_options = null; $widget_settings_url = 'https://app.allaccessible.org/widget'; $account_tier = 'unknown'; if ($has_account) { $api_client = AllAccessible_ApiClient::get_instance(); $site_options = $api_client->get_site_options(); $account_tier = $api_client->get_subscription_tier(); $widget_settings_url = $api_client->get_widget_settings_url(); } $is_paid_tier = in_array($account_tier, array('starter', 'legacy', 'enterprise'), true); ?>


get_site_options(); $account_tier = $api_client->get_subscription_tier(); $is_paid = $api_client->is_paid_account(); $exceeded_limits = $api_client->get_exceeded_limits(); $usage_summary = $api_client->get_usage_summary(); $addon_url = $api_client->get_addon_url(); $audits_url = $api_client->get_audits_url(); $widget_settings_url = $api_client->get_widget_settings_url(); $audit_data = null; $bulk_for_count = $api_client->get_pages_audit_bulk( false, array(), array('tasks', 'aggregation', 'pageviews', 'manifest', 'categorization') ); $aacb_hero_aggregation = (is_array($bulk_for_count) && isset($bulk_for_count['aggregation'])) ? $bulk_for_count['aggregation'] : null; $aacb_hero_task_stats = (is_array($bulk_for_count) && isset($bulk_for_count['tasks'])) ? $bulk_for_count['tasks'] : null; $aacb_hero_pageviews = (is_array($bulk_for_count) && isset($bulk_for_count['pageviews'])) ? $bulk_for_count['pageviews'] : null; $aacb_hero_manifest = (is_array($bulk_for_count) && isset($bulk_for_count['manifest'])) ? $bulk_for_count['manifest'] : null; $aacb_hero_categorization = (is_array($bulk_for_count) && isset($bulk_for_count['categorization'])) ? $bulk_for_count['categorization'] : null; AllAccessible_Debug::console('Dashboard (SettingsPage)', array( 'account_tier' => $account_tier, 'is_paid' => $is_paid, 'exceeded_limits' => $exceeded_limits, 'site_options_type' => is_object($site_options) ? get_class($site_options) : gettype($site_options), 'bulk_is_error' => is_wp_error($bulk_for_count), 'bulk_error' => is_wp_error($bulk_for_count) ? $bulk_for_count->get_error_message() : null, 'bulk_keys' => is_array($bulk_for_count) ? array_keys($bulk_for_count) : null, 'aggregation' => $aacb_hero_aggregation, 'task_stats' => $aacb_hero_task_stats, 'pageviews' => $aacb_hero_pageviews, 'manifest' => $aacb_hero_manifest, 'categorization_keys'=> is_array($aacb_hero_categorization) ? array_keys($aacb_hero_categorization) : null, )); } $is_paid_tier = in_array($account_tier, array('starter', 'legacy', 'enterprise')); $features_list = array(); if (is_object($site_options) && isset($site_options->features) && is_array($site_options->features)) { $features_list = $site_options->features; } elseif (is_array($site_options) && isset($site_options['features']) && is_array($site_options['features'])) { $features_list = $site_options['features']; } $has_agentic = in_array('agentic_fixes_approve', $features_list, true); $is_legacy_v1 = ($account_tier === 'legacy') || ($has_account && !$has_agentic && $is_paid_tier); $has_reports = in_array('reports_export', $features_list, true) || in_array('pdf_export', $features_list, true); // Deep-link site token from /validate. Used for app-side links // (Reports quick action, Tasks page, etc). $deeplink_token = ''; if (is_object($site_options) && !empty($site_options->siteID)) { $deeplink_token = (string) $site_options->siteID; } $deeplink_base = aacb_app_url(); // Legacy V1 → migration offer. Centralized in ApiClient so every // legacy upgrade CTA across the plugin points at the same URL. // $api_client only exists inside the $has_account block above; guard so // an unconfigured/deauthorized site doesn't fatal here (the migration // CTAs that read $migration_url only render for legacy accounts anyway). $migration_url = ''; if ($has_account && isset($api_client)) { $migration_url = $api_client->get_migration_url(); } $show_success = isset($_GET['wizard']) && $_GET['wizard'] === 'complete'; ?> publish ?? 0) + (int) ($aacb_po->publish ?? 0); } // 2.2: `audited_count` is a 30-day window on the API side (mirrors // the app's Page Details default). The hero asks a different // question — "how many of MY published pages have ever been // checked?" — so count it from the per-page rows the same way the // Pages list column does, matched by post id or canonical URL. // Falls back to the 30-day figure only when rows are unavailable. $aacb_rows_audited = null; $aacb_rows_ts = array(); if (is_array($bulk_for_count) && isset($bulk_for_count['pages']) && is_array($bulk_for_count['pages']) && ($extras_hero['pages_total_local'] ?? 0) > 0 && class_exists('AllAccessible_UrlCanonicalizer')) { $aacb_local_ids = get_posts(array( 'post_type' => array('page', 'post'), 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => 500, 'no_found_rows' => true, 'suppress_filters' => true, )); if (is_array($aacb_local_ids) && !empty($aacb_local_ids)) { $aacb_id_set = array_fill_keys(array_map('intval', $aacb_local_ids), true); $aacb_url_set = array(); foreach ($aacb_local_ids as $aacb_lid) { $aacb_canon = AllAccessible_UrlCanonicalizer::for_post((int) $aacb_lid); if ($aacb_canon !== '') $aacb_url_set[$aacb_canon] = (int) $aacb_lid; } $aacb_seen = array(); foreach ($bulk_for_count['pages'] as $aacb_row) { if (!is_array($aacb_row)) continue; $aacb_status = (string) ($aacb_row['audit_status'] ?? ''); $aacb_last = (string) ($aacb_row['last_scan'] ?? ''); if ($aacb_last === '' && ($aacb_status === '' || $aacb_status === 'never')) continue; // discovered, never audited $aacb_match = 0; if (!empty($aacb_row['post_id']) && isset($aacb_id_set[(int) $aacb_row['post_id']])) { $aacb_match = (int) $aacb_row['post_id']; } elseif (!empty($aacb_row['page_url'])) { $aacb_c = AllAccessible_UrlCanonicalizer::canonicalize((string) $aacb_row['page_url']); if ($aacb_c !== '' && isset($aacb_url_set[$aacb_c])) $aacb_match = $aacb_url_set[$aacb_c]; } if ($aacb_match === 0 || isset($aacb_seen[$aacb_match])) continue; $aacb_seen[$aacb_match] = true; $aacb_ts = $aacb_last !== '' ? strtotime($aacb_last) : false; if ($aacb_ts) $aacb_rows_ts[] = (int) $aacb_ts; } $aacb_rows_audited = count($aacb_seen); } } if ($aacb_rows_audited !== null) { $extras_hero['pages_scanned'] = $aacb_rows_audited; $extras_hero['pages_audited_ts'] = $aacb_rows_ts; // per-page last_scan, for freshness } elseif ($live_audited !== null) { $extras_hero['pages_scanned'] = $live_audited; } elseif (isset($aacb_hero_aggregation) && is_array($aacb_hero_aggregation)) { $extras_hero['pages_scanned'] = isset($aacb_hero_aggregation['totalPagesScanned']) ? (int) $aacb_hero_aggregation['totalPagesScanned'] : null; } if (is_array($bulk_for_count) && isset($bulk_for_count['images']) && is_array($bulk_for_count['images'])) { $extras_hero['images_total'] = (int) ($bulk_for_count['images']['total'] ?? 0); $extras_hero['images_ai_generated'] = (int) ($bulk_for_count['images']['ai_generated'] ?? 0); $extras_hero['images_pending'] = (int) ($bulk_for_count['images']['pending'] ?? 0); } if (isset($aacb_hero_manifest) && is_array($aacb_hero_manifest)) { $cs = is_array($aacb_hero_manifest['countsByStatus'] ?? null) ? $aacb_hero_manifest['countsByStatus'] : array(); $extras_hero['pending_fixes'] = (int) ($cs['draft'] ?? 0); $extras_hero['active_fixes'] = (int) ($cs['approved'] ?? 0); } if (is_array($aacb_hero_task_stats) && !empty($aacb_hero_task_stats['has_audit_data'])) { $extras_hero['has_task_data'] = true; $extras_hero['ai_resolved_count'] = (int) ($aacb_hero_task_stats['ai_resolved_count'] ?? 0); $extras_hero['manual_required_count'] = (int) ($aacb_hero_task_stats['manual_required_count'] ?? 0); $extras_hero['completed_count'] = (int) ($aacb_hero_task_stats['completed_count'] ?? 0); $extras_hero['reopened_count'] = (int) ($aacb_hero_task_stats['reopened_count'] ?? 0); $extras_hero['ignored_count'] = (int) ($aacb_hero_task_stats['ignored_count'] ?? 0); $extras_hero['total_actionable'] = (int) ($aacb_hero_task_stats['total_actionable'] ?? 0); } $usage_bars = array(); $us = is_object($site_options) ? ($site_options->usageSummary ?? null) : null; if (is_array($aacb_hero_pageviews) && !empty($aacb_hero_pageviews['limit'])) { $pv_lim = (int) $aacb_hero_pageviews['limit']; $pv_cur = (int) ($aacb_hero_pageviews['current'] ?? 0); $usage_bars[] = array( 'label' => __('Pageviews', 'allaccessible'), 'current' => $pv_cur, 'limit' => $pv_lim, 'pct' => (int) round(($pv_cur / max(1, $pv_lim)) * 100), ); } elseif (is_object($us) && isset($us->pageviews_monthly)) { $node = $us->pageviews_monthly; $limit = isset($node->limit) ? (int) $node->limit : 0; if ($limit > 0) { $current = isset($node->current) ? (int) $node->current : (isset($node->used) ? (int) $node->used : 0); $pct = isset($node->percent) ? (int) round((float) $node->percent) : (int) round(($current / max(1, $limit)) * 100); $usage_bars[] = array( 'label' => __('Pageviews', 'allaccessible'), 'current' => $current, 'limit' => $limit, 'pct' => $pct, ); } } $ai_quota = is_object($site_options) ? ($site_options->aiQuota ?? null) : null; $ai_lifetime = is_object($site_options) ? ($site_options->aiInteractionsLifetime ?? null) : null; $extras_hero['ai_quota'] = $ai_quota; $extras_hero['ai_interactions_lifetime'] = $ai_lifetime; $extras_hero['usage_bars'] = $usage_bars; } $fixes_pending_count = $extras_hero['pending_fixes'] ?? 0; $fixes_active_count = $extras_hero['active_fixes'] ?? 0; $image_slug = (class_exists('ImageManagerPage') && defined('ImageManagerPage::PAGE_SLUG')) ? ImageManagerPage::PAGE_SLUG : 'aacb-image-manager'; ?>


get_scan_schedule(); aacb_render_overview_hero($extras_hero, array( 'has_agentic' => $has_agentic, 'fixes_url' => isset($extras_hero['fixes_url']) ? $extras_hero['fixes_url'] : admin_url('admin.php?page=aacb-agentic-fixes'), 'issues_anchor' => '#aacb-overview-issues', // "View pages" → the native WordPress Pages list (stays in // WP, no app bounce). 'pages_url' => admin_url('edit.php?post_type=page'), 'schedule' => is_wp_error($aacb_schedule) ? null : $aacb_schedule, )); ?> 'danger', 'serious' => 'warn', 'moderate' => 'primary', 'minor' => 'ok', ); $key = strtolower((string) $s); return isset($map[$key]) ? $map[$key] : 'primary'; }; $status_label = static function ($st) { $map = array( 'ai_resolved' => __('AI Resolved', 'allaccessible'), 'manual_required' => __('Manual', 'allaccessible'), 'completed' => __('Completed', 'allaccessible'), 'reopened' => __('Reopened', 'allaccessible'), 'ignored' => __('Ignored', 'allaccessible'), ); return isset($map[$st]) ? $map[$st] : ucfirst(str_replace('_', ' ', (string) $st)); }; ?>
—
0) : ?>

' . implode(', ', $limit_names) . '' ); ?>

get_subscription_tier(); $usage_summary = $api_client->get_usage_summary(); $addon_url = $api_client->get_addon_url(); $billing_url = method_exists($api_client, 'get_billing_url') ? $api_client->get_billing_url() : 'https://app.allaccessible.org/billing'; $widget_url = $api_client->get_widget_settings_url(); $exceeded_limits = $api_client->get_exceeded_limits(); $is_paid_tier = in_array($account_tier, array('starter', 'legacy', 'enterprise', 'trial'), true); $is_developer = in_array($account_tier, array('enterprise', 'legacy'), true); if (method_exists($api_client, 'get_site_id')) { $fresh_site_id = $api_client->get_site_id(); if (!empty($fresh_site_id)) { $site_id = (string) $fresh_site_id; } } } // Quick toggles — stored inside the existing aacb_options array so // we never introduce a new option key. $aacb_options = (array) get_option('aacb_options', array()); $headless_mode = !empty($aacb_options['headless_mode']); $remove_branding = !empty($aacb_options['remove_branding']); $debug_mode = !empty($aacb_options['debug_mode']); AllAccessible_Debug::console('Account (SettingsPage)', array( 'has_account' => $has_account, 'account_id' => $account_id, 'site_id' => $site_id, 'tier' => $account_tier, 'is_paid' => $is_paid_tier, 'is_developer' => $is_developer, 'exceeded_limits' => $exceeded_limits, 'addon_url' => $addon_url, 'billing_url' => $billing_url, 'widget_url' => $widget_url, 'toggles' => array( 'headless_mode' => $headless_mode, 'remove_branding' => $remove_branding, 'debug_mode' => $debug_mode, ), 'usage_summary' => $usage_summary, )); $pv_used = null; $pv_limit = null; $pv_pct = null; $pv_node = null; if (is_object($usage_summary) && isset($usage_summary->pageviews_monthly)) { $pv_node = $usage_summary->pageviews_monthly; $pv_used = isset($pv_node->current) ? (int) $pv_node->current : (isset($pv_node->used) ? (int) $pv_node->used : null); $pv_limit = isset($pv_node->limit) ? (int) $pv_node->limit : null; $pv_pct = isset($pv_node->percent) ? (int) round((float) $pv_node->percent) : (($pv_limit && $pv_used !== null) ? min(100, (int) round(($pv_used / max(1, $pv_limit)) * 100)) : null); } elseif (is_array($usage_summary) && isset($usage_summary['pageviews_monthly'])) { $pv_node = $usage_summary['pageviews_monthly']; $pv_used = isset($pv_node['current']) ? (int) $pv_node['current'] : (isset($pv_node['used']) ? (int) $pv_node['used'] : null); $pv_limit = isset($pv_node['limit']) ? (int) $pv_node['limit'] : null; $pv_pct = isset($pv_node['percent']) ? (int) round((float) $pv_node['percent']) : (($pv_limit && $pv_used !== null) ? min(100, (int) round(($pv_used / max(1, $pv_limit)) * 100)) : null); } if ($pv_pct === null) { $pv_pct = 0; } $pv_badge_variant = $pv_pct >= 90 ? 'danger' : ($pv_pct >= 75 ? 'warn' : 'ok'); $tier_meta = self::tier_meta($account_tier); // Nonces (preserve existing names; the toggles nonce is additive) $reset_nonce = wp_create_nonce('aacb_reset_plugin'); $clear_nonce = wp_create_nonce('aacb_clear_cache'); $toggles_nonce = wp_create_nonce('aacb_save_quick_toggles'); // Initial tab (query string-driven; JS handles client-side swap) $allowed_tabs = array('connection', 'plan', 'widget', 'advanced'); $active_tab = isset($_GET['tab']) ? sanitize_key((string) $_GET['tab']) : 'connection'; if (!in_array($active_tab, $allowed_tabs, true)) { $active_tab = 'connection'; } ?>


__('Connection', 'allaccessible'), 'plan' => __('Plan & Usage', 'allaccessible'), 'widget' => __('Widget', 'allaccessible'), 'advanced' => __('Advanced', 'allaccessible'), ); foreach ($tabs as $tab_id => $tab_label) : $is_active = ($tab_id === $active_tab); ?>
>

'; AllAccessible_ConnectionStatusCard::render(); echo '
'; } ?>
>

%
= 90 ? 'var(--aacx-danger-500)' : ($pv_pct >= 75 ? 'var(--aacx-warn-500)' : 'var(--aacx-ok-500)'); ?>

' . esc_html(number_format_i18n($pv_used)) . '', '' . esc_html(number_format_i18n($pv_limit)) . '' ); ?>

= 75): ?>
= 90): ?>

>

disabled aria-disabled="true">

See plans.', 'allaccessible'), array('a' => array('href' => true, 'target' => true, 'rel' => true))), esc_url($addon_url) ); ?>

disabled aria-disabled="true">

See plans.', 'allaccessible'), array('a' => array('href' => true, 'target' => true, 'rel' => true))), esc_url($addon_url) ); ?>

>

__('Unauthorized', 'allaccessible')), 403); } check_ajax_referer('aacb_save_quick_toggles', '_wpnonce'); $existing = (array) get_option('aacb_options', array()); $existing['headless_mode'] = isset($_POST['headless_mode']) && (string) $_POST['headless_mode'] === '1'; $existing['remove_branding'] = isset($_POST['remove_branding']) && (string) $_POST['remove_branding'] === '1'; $existing['debug_mode'] = isset($_POST['debug_mode']) && (string) $_POST['debug_mode'] === '1'; $existing['sentry_disabled'] = isset($_POST['sentry_disabled']) && (string) $_POST['sentry_disabled'] === '1'; update_option('aacb_options', $existing); wp_send_json_success(array( 'message' => __('Settings saved', 'allaccessible'), )); } /** * Tier metadata used by the redesigned account header + plan card. * v2_variant maps to the design-system badge variant so the header * pill and Plan tab never drift out of sync. */ /** * State for the Getting-started checklist, from values the Dashboard * already loaded. No extra API calls. * * @param object|WP_Error|null $schedule get_scan_schedule() result */ private static function checklist_context($schedule, array $extras_hero, $has_agentic) { $scanned = (is_object($schedule) && !is_wp_error($schedule) && !empty($schedule->lastScanAt)) || !empty($extras_hero['site_score']) || (int) (isset($extras_hero['pages_scanned']) ? $extras_hero['pages_scanned'] : 0) > 0; return array( 'has_account' => (string) get_option('aacb_accountID') !== '', 'scanned' => $scanned, 'has_agentic' => (bool) $has_agentic, 'active_fixes' => (int) (isset($extras_hero['active_fixes']) ? $extras_hero['active_fixes'] : 0), 'statement' => class_exists('AllAccessible_StatementPage') ? AllAccessible_StatementPage::status() : array(), 'urls' => array( 'scan' => admin_url('admin.php?page=aacb-agentic-fixes'), 'fixes' => admin_url('admin.php?page=aacb-agentic-fixes'), 'statement' => '#aacb-statement-card', ), ); } private static function tier_meta($tier) { switch ($tier) { case 'trial': return array( 'label' => __('Trial', 'allaccessible'), 'long_label' => __('Premium Trial — 7 days free', 'allaccessible'), 'v2_variant' => 'primary', ); case 'starter': return array( 'label' => __('Starter', 'allaccessible'), 'long_label' => __('Starter', 'allaccessible'), 'v2_variant' => 'ok', ); case 'enterprise': return array( 'label' => __('Enterprise', 'allaccessible'), 'long_label' => __('Enterprise', 'allaccessible'), 'v2_variant' => 'ai', ); case 'legacy': return array( 'label' => __('Legacy', 'allaccessible'), 'long_label' => __('Legacy Premium', 'allaccessible'), 'v2_variant' => 'primary', ); case 'free': return array( 'label' => __('Free', 'allaccessible'), 'long_label' => __('Free', 'allaccessible'), 'v2_variant' => 'primary', ); default: return array( 'label' => ucfirst((string) $tier), 'long_label' => sprintf( /* translators: %s: tier name (e.g., "Enterprise") */ __('%s account', 'allaccessible'), ucfirst((string) $tier) ), 'v2_variant' => 'primary', ); } } /** * Plan benefit checklist for the Plan card. * * Brand voice: outcome-focused, no fear marketing, no internal stack * components. AI mentions wrap "AllAccessible AI" in the v2 AI badge. */ private static function tier_benefits($tier, $live_limits = array()) { $ai_badge = '' . esc_html__('AllAccessible AI', 'allaccessible') . ''; $pv_limit = isset($live_limits['pageviews_monthly']) ? (int) $live_limits['pageviews_monthly'] : null; $pv_benefit = $pv_limit && $pv_limit > 0 ? sprintf( /* translators: %s: monthly pageview limit (e.g. "10,000") */ __('Up to %s pageviews per month', 'allaccessible'), number_format_i18n($pv_limit) ) : __('Pageview cap based on your plan', 'allaccessible'); $ai_benefit = sprintf( /* translators: %s: AI badge */ __('Unlimited %s interactions — alt text and agentic fix proposals, no monthly cap', 'allaccessible'), $ai_badge ); switch ($tier) { case 'enterprise': return array( __('Unlimited pageviews and sites', 'allaccessible'), sprintf(/* translators: %s: AI badge */ __('Human-in-the-loop agentic remediation powered by %s', 'allaccessible'), $ai_badge), __('Priority support and dedicated success manager', 'allaccessible'), __('Custom branding and white-label widget', 'allaccessible'), __('Single sign-on (SSO) and audit log export', 'allaccessible'), ); case 'starter': return array( $pv_benefit, $ai_benefit, __('Continuous site scans and accessibility scoring', 'allaccessible'), __('Hide widget branding', 'allaccessible'), __('Email support', 'allaccessible'), ); case 'trial': return array( __('Full premium widget for 7 days', 'allaccessible'), sprintf(/* translators: %s: AI badge */ __('Try %s agents on your real site', 'allaccessible'), $ai_badge), __('Cancel any time — no card hold', 'allaccessible'), ); case 'legacy': return array( __('Premium widget on the legacy plan', 'allaccessible'), sprintf(/* translators: %s: AI badge */ __('Includes %s agent features', 'allaccessible'), $ai_badge), __('Continuous accessibility scoring', 'allaccessible'), ); case 'free': default: return array( __('Accessibility widget on every page', 'allaccessible'), __('Keyboard navigation and screen reader helpers', 'allaccessible'), sprintf(/* translators: %s: AI badge */ __('Upgrade for agentic remediation with %s', 'allaccessible'), $ai_badge), ); } } } // Initialize settings page add_action('plugins_loaded', function() { AllAccessible_SettingsPage::get_instance(); });