get_error_message(); return stripos($msg, 'ssl') !== false || stripos($msg, 'certificate') !== false || stripos($msg, 'curl error 60') !== false || stripos($msg, 'curl error 77') !== false || stripos($msg, 'curl error 35') !== false; } function aerochat_push_item(array $item) { $api_key = get_option('aerochat_api_key', ''); if (empty($api_key)) return false; $url = aerochat_get_api_url() . '/bot/wordpress/webhook/content'; $response = aerochat_remote_post($url, [ 'body' => wp_json_encode($item), 'headers' => [ 'Content-Type' => 'application/json', 'X-AeroChat-API-Key' => $api_key, ], 'timeout' => 15, ]); if (is_wp_error($response)) { aerochat_log('Push failed: ' . $response->get_error_message()); return false; } return wp_remote_retrieve_response_code($response) >= 200 && wp_remote_retrieve_response_code($response) < 300; } function aerochat_push_batch(array $items) { $api_key = get_option('aerochat_api_key', ''); if (empty($api_key) || empty($items)) return false; $url = aerochat_get_api_url() . '/bot/wordpress/webhook/batch'; $response = aerochat_remote_post($url, [ 'body' => wp_json_encode(['items' => $items]), 'headers' => [ 'Content-Type' => 'application/json', 'X-AeroChat-API-Key' => $api_key, ], 'timeout' => 60, ]); if (is_wp_error($response)) { aerochat_log('Batch push failed: ' . $response->get_error_message()); return false; } return wp_remote_retrieve_response_code($response) >= 200 && wp_remote_retrieve_response_code($response) < 300; } /** * Ping the AeroChat backend to verify the API key and fetch connection data. * * Always returns a structured array so callers can surface the specific * failure mode to the merchant (bad key vs. unreachable vs. wrong URL). * * Success: ['ok' => true, 'company_id' => int, 'widget_key' => string, ...] * Failure: ['ok' => false, 'reason' => , 'detail' => ] * * Failure reasons: * - missing_api_key : no API key saved in WP options * - network : wp_remote_post returned WP_Error (DNS/TLS/timeout) * - http_ : backend responded with non-2xx (e.g. http_401) */ function aerochat_ping() { $api_key = get_option('aerochat_api_key', ''); if (empty($api_key)) { return ['ok' => false, 'reason' => 'missing_api_key', 'detail' => 'No API key saved']; } $url = aerochat_get_api_url() . '/bot/wordpress/webhook/ping'; $response = aerochat_remote_post($url, [ 'body' => wp_json_encode([ 'site_url' => site_url(), 'enabled' => (bool) get_option('aerochat_enabled'), ]), 'headers' => [ 'Content-Type' => 'application/json', 'X-AeroChat-API-Key' => $api_key, ], 'timeout' => 10, ]); if (is_wp_error($response)) { $detail = $response->get_error_message(); aerochat_log('ping network error url=' . $url . ' detail=' . $detail); return ['ok' => false, 'reason' => 'network', 'detail' => $detail]; } $code = wp_remote_retrieve_response_code($response); if ($code < 200 || $code >= 300) { $body = wp_remote_retrieve_body($response); aerochat_log('ping http ' . $code . ' url=' . $url . ' body=' . substr((string) $body, 0, 500)); return ['ok' => false, 'reason' => 'http_' . $code, 'detail' => (string) $body]; } $body = json_decode(wp_remote_retrieve_body($response), true); $payload = is_array($body) ? $body : []; return array_merge(['ok' => true], $payload); } /** * Backwards-compatible boolean wrapper around aerochat_ping(). */ function aerochat_is_connected() { $p = aerochat_ping(); return is_array($p) && !empty($p['ok']); } /** * Human-readable message for a failed aerochat_ping() return value. */ function aerochat_ping_error_message(array $ping) { $reason = isset($ping['reason']) ? $ping['reason'] : 'unknown'; $detail = isset($ping['detail']) ? $ping['detail'] : ''; if ($reason === 'missing_api_key') { return __('No API key saved. Paste the key from your AeroChat dashboard and save.', 'aerochat'); } if ($reason === 'network') { return sprintf( /* translators: 1: backend URL, 2: error details */ __('Could not reach the AeroChat backend (%1$s). Check that your server can make outbound HTTPS calls. Details: %2$s', 'aerochat'), aerochat_get_api_url(), $detail ); } if ($reason === 'http_401') { return __('API key rejected by the backend (HTTP 401). Generate a fresh key in your AeroChat dashboard and paste it again.', 'aerochat'); } if ($reason === 'site_already_claimed') { return $detail ?: __('This WordPress site is already linked to another AeroChat account. Disconnect it from that account first.', 'aerochat'); } if (strpos($reason, 'http_') === 0) { $suffix = ''; if ($detail) { /* translators: %s: error details returned by the backend */ $suffix = sprintf(__('Details: %s', 'aerochat'), $detail); } return sprintf( /* translators: 1: HTTP status (e.g. "HTTP 500"), 2: optional details suffix */ __('Backend returned %1$s. %2$s', 'aerochat'), str_replace('http_', 'HTTP ', $reason), $suffix ); } return __('Connection failed.', 'aerochat'); } // ══════════════════════════════════════════════════════════════════════════════ // Content collectors // ══════════════════════════════════════════════════════════════════════════════ function aerochat_collect_pages() { $pages = get_posts([ 'post_type' => 'page', 'post_status' => 'publish', 'numberposts' => -1, ]); $items = []; foreach ($pages as $page) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- intentionally applying WP core's the_content filter to render the page body. $html = apply_filters('the_content', $page->post_content); $items[] = [ 'wordpress_id' => 'wp_page_' . $page->ID, 'name' => $page->post_title, 'url' => get_permalink($page->ID), 'source_type' => 'Page', 'category' => 'pages', 'content_html' => $html, 'content_text' => wp_strip_all_tags($html), 'action' => 'upsert', ]; } return $items; } function aerochat_collect_posts() { $posts = get_posts([ 'post_type' => 'post', 'post_status' => 'publish', 'numberposts' => -1, ]); $items = []; foreach ($posts as $post) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- intentionally applying WP core's the_content filter to render the post body. $html = apply_filters('the_content', $post->post_content); $items[] = [ 'wordpress_id' => 'wp_post_' . $post->ID, 'name' => $post->post_title, 'url' => get_permalink($post->ID), 'source_type' => 'Post', 'category' => 'pages', 'content_html' => $html, 'content_text' => wp_strip_all_tags($html), 'action' => 'upsert', ]; } return $items; } function aerochat_collect_products() { if (!class_exists('WooCommerce')) return []; $products = wc_get_products([ 'status' => 'publish', 'limit' => -1, ]); $items = []; foreach ($products as $product) { $variants = []; $prices = []; $option_summary = []; if ($product->is_type('variable')) { $variations = $product->get_available_variations(); foreach ($variations as $v) { $variation = wc_get_product($v['variation_id']); if (!$variation) continue; $v_price = (float) $variation->get_price(); $prices[] = $v_price; $attrs = []; foreach ($v['attributes'] as $attr_key => $attr_val) { $attr_name = str_replace('attribute_pa_', '', $attr_key); $attr_name = str_replace('attribute_', '', $attr_name); $attr_name = ucfirst($attr_name); $attrs[$attr_name] = $attr_val; if (!isset($option_summary[$attr_name])) { $option_summary[$attr_name] = []; } $option_summary[$attr_name][] = $attr_val; } $variants[] = array_merge([ 'variant_id' => 'wp_variation_' . $v['variation_id'], 'title' => implode(' - ', array_filter($attrs)), 'price' => $v_price, 'inventory' => $variation->get_stock_quantity() ?: 0, ], $attrs); } } if (empty($variants)) { $p_price = (float) $product->get_price(); $prices[] = $p_price; $variants[] = [ 'variant_id' => 'wp_product_' . $product->get_id(), 'title' => 'Default', 'price' => $p_price, 'inventory' => $product->get_stock_quantity() ?: 0, ]; } // Deduplicate option values foreach ($option_summary as $k => $vals) { $option_summary[$k] = array_unique($vals); } $variant_options_parts = []; foreach ($option_summary as $k => $vals) { $variant_options_parts[] = $k . ': ' . implode(', ', $vals); } $variant_options_str = implode(' | ', $variant_options_parts); $tag_terms = wp_get_post_terms($product->get_id(), 'product_tag', ['fields' => 'names']); $tags = is_array($tag_terms) ? implode(', ', $tag_terms) : ''; $image_id = $product->get_image_id(); $image_url = $image_id ? wp_get_attachment_url($image_id) : null; $desc = $product->get_description() ?: $product->get_short_description(); $product_data = [ 'product_id' => 'wp_product_' . $product->get_id(), 'name' => $product->get_name(), 'description' => wp_strip_all_tags($desc), 'tags' => $tags, 'link' => $product->get_permalink(), 'quantity' => $product->get_stock_quantity() ?: array_sum(array_column($variants, 'inventory')), 'price_min' => !empty($prices) ? min($prices) : 0.0, 'price_max' => !empty($prices) ? max($prices) : 0.0, 'image_url' => $image_url, 'variants' => $variants, 'variant_options' => $variant_options_str, ]; $items[] = [ 'wordpress_id' => 'wp_product_' . $product->get_id(), 'name' => $product->get_name(), 'url' => $product->get_permalink(), 'source_type' => 'Product', 'category' => 'products', 'content_html' => $desc, 'content_text' => 'Product: ' . $product->get_name() . "\n" . wp_strip_all_tags($desc) . "\nTags: " . $tags, 'structured_data' => $product_data, 'action' => 'upsert', ]; } return $items; } function aerochat_collect_coupons() { if (!class_exists('WooCommerce')) return []; $coupon_posts = get_posts([ 'post_type' => 'shop_coupon', 'post_status' => 'publish', 'numberposts' => -1, ]); $type_map = [ 'percent' => 'percentage', 'fixed_cart' => 'fixed_amount', 'fixed_product' => 'fixed_amount', ]; $items = []; foreach ($coupon_posts as $cp) { $coupon = new WC_Coupon($cp->ID); $wc_type = $coupon->get_discount_type(); $discount_type = isset($type_map[$wc_type]) ? $type_map[$wc_type] : $wc_type; $amount = (float) $coupon->get_amount(); $value = ($discount_type === 'percentage') ? $amount / 100 : $amount; $code = $coupon->get_code(); $expires = $coupon->get_date_expires(); $expires_str = $expires ? $expires->date('Y-m-d\TH:i:s') : null; $suffix = ($discount_type === 'percentage') ? '%' : ''; $description = $code . ' -- ' . $amount . $suffix . ' off'; $items[] = [ 'wordpress_id' => 'wp_coupon_' . $cp->ID, 'name' => strtoupper($code), 'url' => '', 'source_type' => 'Coupon', 'category' => 'promotions', 'content_html' => '', 'content_text' => $description, 'structured_data' => [ 'shopify_gid' => 'wp_coupon_' . $cp->ID, 'title' => strtoupper($code), 'discount_type' => $discount_type, 'value' => $value, 'currency' => null, 'code' => $code, 'is_automatic' => 0, 'starts_at' => null, 'ends_at' => $expires_str, 'summary' => $amount . $suffix . ' off', 'description' => $description, 'buy_quantity' => null, 'get_quantity' => null, ], 'action' => 'upsert', ]; } return $items; } function aerochat_collect_shipping_zones() { if (!class_exists('WC_Shipping_Zones')) return []; $zones = WC_Shipping_Zones::get_zones(); // Include zone 0 (rest of world) $zone_0 = new WC_Shipping_Zone(0); $zones[0] = [ 'id' => 0, 'zone_name' => $zone_0->get_zone_name(), 'shipping_methods' => $zone_0->get_shipping_methods(), 'zone_locations' => $zone_0->get_zone_locations(), ]; $items = []; foreach ($zones as $zone_data) { $zone_id = $zone_data['id'] ?? 0; $zone_name = $zone_data['zone_name'] ?? 'Zone #' . $zone_id; $methods = $zone_data['shipping_methods'] ?? []; $locations = $zone_data['zone_locations'] ?? []; $countries = []; foreach ($locations as $loc) { if (isset($loc->type) && $loc->type === 'country') { $countries[] = ['name' => $loc->code, 'code' => $loc->code]; } elseif (is_array($loc) && ($loc['type'] ?? '') === 'country') { $countries[] = ['name' => $loc['code'], 'code' => $loc['code']]; } } $shipping_data = []; foreach ($methods as $method) { if (!$method->is_enabled()) continue; $method_id = $method->id; $method_title = $method->get_title(); $cost = 0.0; $is_free = ($method_id === 'free_shipping') ? 1 : 0; $min_amount = null; if (method_exists($method, 'get_option')) { $cost_raw = $method->get_option('cost', '0'); $cost = is_numeric($cost_raw) ? (float) $cost_raw : 0.0; if ($is_free) { $min_raw = $method->get_option('min_amount', ''); $min_amount = is_numeric($min_raw) ? (float) $min_raw : null; } } $desc = $method_title . ' to ' . $zone_name; if ($is_free && $min_amount) { $desc .= ' -- Free on orders over $' . number_format($min_amount, 2); } elseif (!$is_free && $cost > 0) { $desc .= ' -- $' . number_format($cost, 2); } $shipping_data[] = [ 'profile_name' => 'Default', 'zone_name' => $zone_name, 'countries' => $countries, 'method_name' => $method_title, 'price' => $cost, 'currency' => get_woocommerce_currency(), 'is_free' => $is_free, 'min_order_amount'=> $min_amount, 'condition_type' => null, 'description' => $desc, ]; } if (!empty($shipping_data)) { $items[] = [ 'wordpress_id' => 'wp_shipping_zone_' . $zone_id, 'name' => 'Shipping: ' . $zone_name, 'url' => '', 'source_type' => 'Shipping', 'category' => 'promotions', 'content_html' => '', 'content_text' => implode("\n", array_column($shipping_data, 'description')), 'structured_data' => $shipping_data, 'action' => 'upsert', ]; } } return $items; } // ══════════════════════════════════════════════════════════════════════════════ // Full sync // ══════════════════════════════════════════════════════════════════════════════ // Max items sent per /webhook/batch POST. Keeps each request small enough to // stay under PHP post_max_size, host body-size limits, and our 60s HTTP // timeout — large stores can ship thousands of items via multiple small POSTs // instead of one giant request that times out. define('AEROCHAT_BATCH_SIZE', 100); function aerochat_full_sync() { $all_items = array_merge( aerochat_collect_pages(), aerochat_collect_posts(), aerochat_collect_products(), aerochat_collect_coupons(), aerochat_collect_shipping_zones() ); if (empty($all_items)) return false; $chunks = array_chunk($all_items, AEROCHAT_BATCH_SIZE); $all_ok = true; foreach ($chunks as $chunk) { if (!aerochat_push_batch($chunk)) { $all_ok = false; } } if ($all_ok) { update_option('aerochat_synced', true); update_option('aerochat_last_sync', current_time('mysql')); } return $all_ok; } // ══════════════════════════════════════════════════════════════════════════════ // Real-time hooks (push on content changes) // ══════════════════════════════════════════════════════════════════════════════ class AeroChatHooks { public function __construct() { // Pages & posts add_action('transition_post_status', [$this, 'on_post_status_change'], 10, 3); // WooCommerce (only if active) if (class_exists('WooCommerce')) { add_action('woocommerce_update_product', [$this, 'on_product_save'], 10, 1); add_action('woocommerce_new_product', [$this, 'on_product_save'], 10, 1); add_action('before_delete_post', [$this, 'on_post_delete'], 10, 1); add_action('woocommerce_coupon_options_save', [$this, 'on_coupon_save'], 10, 1); add_action('woocommerce_after_shipping_zone_object_save', [$this, 'on_shipping_zone_save'], 10, 2); } } public function on_post_status_change($new_status, $old_status, $post) { if (!in_array($post->post_type, ['post', 'page'])) return; if (empty(get_option('aerochat_api_key', ''))) return; $type_key = ($post->post_type === 'post') ? 'post' : 'page'; $source_type = ($post->post_type === 'post') ? 'Post' : 'Page'; if ($new_status === 'publish') { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- intentionally applying WP core's the_content filter to render the post body. $html = apply_filters('the_content', $post->post_content); aerochat_push_item([ 'wordpress_id' => 'wp_' . $type_key . '_' . $post->ID, 'name' => $post->post_title, 'url' => get_permalink($post->ID), 'source_type' => $source_type, 'category' => 'pages', 'content_html' => $html, 'content_text' => wp_strip_all_tags($html), 'action' => 'upsert', ]); } elseif ($old_status === 'publish' && in_array($new_status, ['draft', 'trash', 'pending'])) { aerochat_push_item([ 'wordpress_id' => 'wp_' . $type_key . '_' . $post->ID, 'name' => $post->post_title, 'url' => '', 'source_type' => $source_type, 'category' => 'pages', 'action' => 'delete', ]); } } public function on_product_save($product_id) { if (empty(get_option('aerochat_api_key', ''))) return; // Defer to avoid slowing down save wp_schedule_single_event(time() + 5, 'aerochat_deferred_product_push', [$product_id]); } public function on_post_delete($post_id) { $post = get_post($post_id); if (!$post || $post->post_type !== 'product') return; if (empty(get_option('aerochat_api_key', ''))) return; aerochat_push_item([ 'wordpress_id' => 'wp_product_' . $post_id, 'name' => $post->post_title, 'url' => '', 'source_type' => 'Product', 'category' => 'products', 'action' => 'delete', ]); } public function on_coupon_save($coupon_id) { if (empty(get_option('aerochat_api_key', ''))) return; wp_schedule_single_event(time() + 5, 'aerochat_deferred_coupon_push', [$coupon_id]); } public function on_shipping_zone_save($zone, $data) { if (empty(get_option('aerochat_api_key', ''))) return; // Shipping zones are interrelated, push all wp_schedule_single_event(time() + 10, 'aerochat_deferred_shipping_push'); } } // Deferred push handlers add_action('aerochat_deferred_product_push', function($product_id) { $product = wc_get_product($product_id); if (!$product || $product->get_status() !== 'publish') return; // Collect just this product $items = aerochat_collect_products(); foreach ($items as $item) { if ($item['wordpress_id'] === 'wp_product_' . $product_id) { aerochat_push_item($item); return; } } }); add_action('aerochat_deferred_coupon_push', function($coupon_id) { $items = aerochat_collect_coupons(); foreach ($items as $item) { if ($item['wordpress_id'] === 'wp_coupon_' . $coupon_id) { aerochat_push_item($item); return; } } }); add_action('aerochat_deferred_shipping_push', function() { $items = aerochat_collect_shipping_zones(); if (!empty($items)) { aerochat_push_batch($items); } }); // Initialize hooks new AeroChatHooks(); // ══════════════════════════════════════════════════════════════════════════════ // Widget injection // ══════════════════════════════════════════════════════════════════════════════ /** * Enqueue the AeroChat chat widget loader on the front end. * * The loader (chatbox_live.js) reads its ``data-src`` attribute synchronously * during its own initialization. We register the script the normal way with * wp_enqueue_script (so it is properly managed by WordPress) and then attach * the ``async`` and ``data-src`` attributes to the rendered tag via the * ``script_loader_tag`` filter below. * * Rendered output (matches Shopify / WhatsApp / Telegram channel injection): * a script tag whose src is {api_url}/static/chatbox_live.js, marked async, * with a data-src attribute set to {widget_key}. * * ``data-src`` is the bare widget key (company ``secretkey``), NOT a URL — * the loader joins it with its own base URL internally. */ add_action('wp_enqueue_scripts', function() { if (!get_option('aerochat_enabled')) return; $widget_key = get_option('aerochat_widget_key', ''); if (empty($widget_key)) return; wp_enqueue_script( 'aerochat-widget', aerochat_get_api_url() . '/static/chatbox_live.js', [], AEROCHAT_VERSION, true ); }); add_filter('script_loader_tag', function($tag, $handle, $src) { if ($handle !== 'aerochat-widget') return $tag; // Inject the loader's required ``async`` and ``data-src`` attributes onto // the tag WordPress generated for this enqueued handle. $widget_key = get_option('aerochat_widget_key', ''); $attributes = sprintf(' async data-src="%s"', esc_attr($widget_key)); return str_replace(' src=', $attributes . ' src=', $tag); }, 10, 3); // ══════════════════════════════════════════════════════════════════════════════ // AJAX handlers // ══════════════════════════════════════════════════════════════════════════════ add_action('wp_ajax_aerochat_sync', function() { if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Unauthorized', 'aerochat')], 403); } $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (!wp_verify_nonce($nonce, 'aerochat_sync_action')) { wp_send_json_error(['message' => __('Invalid nonce', 'aerochat')], 403); } $result = aerochat_full_sync(); if ($result) { wp_send_json_success(['message' => __('Sync complete', 'aerochat')]); } else { wp_send_json_error(['message' => __('Sync failed. Check your API key.', 'aerochat')]); } }); add_action('wp_ajax_aerochat_toggle', function() { if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Unauthorized', 'aerochat')], 403); } $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (!wp_verify_nonce($nonce, 'aerochat_toggle_action')) { wp_send_json_error(['message' => __('Invalid nonce', 'aerochat')], 403); } $enabled = isset($_POST['enabled']) ? intval(wp_unslash($_POST['enabled'])) : 0; update_option('aerochat_enabled', $enabled); // Fire an immediate ping so the backend sees the flip without waiting // for the next periodic sync — powers the onboarding "Verify Now" gate. aerochat_ping(); wp_send_json_success([ 'message' => $enabled ? __('Chatbot enabled in your theme', 'aerochat') : __('Chatbot disabled in your theme', 'aerochat'), ]); }); add_action('wp_ajax_aerochat_save_settings', function() { if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Unauthorized', 'aerochat')], 403); } $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (!wp_verify_nonce($nonce, 'aerochat_save_settings_action')) { wp_send_json_error(['message' => __('Invalid nonce', 'aerochat')], 403); } // Only the API key is captured from the form now. The widget key is // auto-fetched from the backend on successful ping, and the API URL // is fixed (or overridden via the AEROCHAT_API_URL constant in wp-config). $api_key = isset($_POST['api_key']) ? sanitize_text_field(wp_unslash($_POST['api_key'])) : ''; update_option('aerochat_api_key', $api_key); // Test connection; on success, auto-populate the widget key from the // ping response and trigger an initial content sync. $ping_data = aerochat_ping(); $connected = is_array($ping_data) && !empty($ping_data['ok']); if ($connected) { if (!empty($ping_data['widget_key'])) { update_option('aerochat_widget_key', sanitize_text_field($ping_data['widget_key'])); } // Run the initial sync inline rather than via wp_schedule_single_event. // The chunked push (AEROCHAT_BATCH_SIZE per request) keeps this fast // enough to complete within the merchant's "Save Settings" AJAX call, // and removes the dependency on WP-Cron actually firing. aerochat_full_sync(); } $message = $connected ? __('Settings saved. Connection verified!', 'aerochat') : sprintf( /* translators: %s: human-readable connection error message */ __('Settings saved, but: %s', 'aerochat'), aerochat_ping_error_message(is_array($ping_data) ? $ping_data : []) ); wp_send_json_success([ 'message' => $message, 'connected' => $connected, 'reason' => $connected ? null : (isset($ping_data['reason']) ? $ping_data['reason'] : 'unknown'), 'api_url' => aerochat_get_api_url(), ]); }); // ══════════════════════════════════════════════════════════════════════════════ // Admin UI (Settings + Dashboard) // ══════════════════════════════════════════════════════════════════════════════ class AeroChatAdmin { private $dashboard_hook = ''; private $settings_hook = ''; public function __construct() { add_action('admin_menu', [$this, 'register_menus']); add_action('admin_init', [$this, 'handle_redirect']); add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']); } public function register_menus() { $this->dashboard_hook = add_menu_page( __('AeroChat', 'aerochat'), __('AeroChat', 'aerochat'), 'manage_options', 'aerochat_dashboard', [$this, 'render_dashboard'], 'dashicons-format-chat', 26 ); $this->settings_hook = add_submenu_page( 'aerochat_dashboard', __('AeroChat Settings', 'aerochat'), __('Settings', 'aerochat'), 'manage_options', 'aerochat_settings', [$this, 'render_settings'] ); } /** * Enqueue the admin page scripts and hand their translatable strings and * the AJAX endpoint to JavaScript via wp_localize_script (no inline JS). */ public function enqueue_assets($hook) { if ($hook === $this->settings_hook) { wp_enqueue_script( 'aerochat-admin-settings', plugins_url('assets/js/admin-settings.js', __FILE__), [], AEROCHAT_VERSION, true ); wp_localize_script('aerochat-admin-settings', 'aerochatSettings', [ 'ajaxurl' => admin_url('admin-ajax.php'), 'i18n' => [ 'saving' => __('Saving...', 'aerochat'), 'connected' => '✓ ' . __('Connected', 'aerochat'), 'notConnected' => '✗ ' . __('Not connected', 'aerochat'), 'errorSaving' => __('Error saving settings', 'aerochat'), 'networkError' => __('Network error', 'aerochat'), ], ]); } elseif ($hook === $this->dashboard_hook) { wp_enqueue_script( 'aerochat-admin-dashboard', plugins_url('assets/js/admin-dashboard.js', __FILE__), [], AEROCHAT_VERSION, true ); wp_localize_script('aerochat-admin-dashboard', 'aerochatDashboard', [ 'ajaxurl' => admin_url('admin-ajax.php'), 'i18n' => [ 'networkError' => __('Network error.', 'aerochat'), 'syncing' => __('Syncing... Please wait.', 'aerochat'), 'syncComplete' => __('Sync complete!', 'aerochat'), 'syncFailed' => __('Sync failed.', 'aerochat'), ], ]); } } public function handle_redirect() { if (get_option('aerochat_do_redirect')) { delete_option('aerochat_do_redirect'); wp_safe_redirect(admin_url('admin.php?page=aerochat_settings')); exit; } } public function render_settings() { $api_key = get_option('aerochat_api_key', ''); $connected = !empty($api_key) && aerochat_is_connected(); ?>

WordPress > Generate API Key).', 'aerochat'), '' . esc_html__('AeroChat Dashboard', 'aerochat') . '' ); ?>

✓ ✗

'post', 'post_status' => 'publish', 'numberposts' => -1])); $page_count = count(get_posts(['post_type' => 'page', 'post_status' => 'publish', 'numberposts' => -1])); $product_count = $has_wc ? (int) wp_count_posts('product')->publish : 0; $coupon_count = $has_wc ? (int) wp_count_posts('shop_coupon')->publish : 0; ?>

' . esc_html__('configure your API key', 'aerochat') . '' ); ?>

/>


' . esc_html($last_sync) . '' ); ?>