is_woocommerce_active()) { add_action('admin_notices', array($this, 'woocommerce_missing_notice')); return; } // Initialize plugin $this->init(); } /** * Get instance * * @return Acoustic_Connect_Woo */ public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } /** * Check if WooCommerce is active * * @return bool */ private function is_woocommerce_active() { // Check if WooCommerce class exists if (class_exists('WooCommerce')) { return true; } // Fallback: check active plugins if (!function_exists('is_plugin_active')) { include_once ABSPATH . 'wp-admin/includes/plugin.php'; } if (function_exists('is_plugin_active')) { return is_plugin_active('woocommerce/woocommerce.php'); } return false; } /** * WooCommerce missing notice */ public function woocommerce_missing_notice() { $message = sprintf( /* translators: %s: WooCommerce plugin link */ esc_html__('Acoustic Connect for WooCommerce requires WooCommerce to be installed and active. You can download %s here.', 'acoustic-connect-woo'), 'WooCommerce' ); echo '

' . wp_kses_post($message) . '

'; } /** * Initialize plugin */ private function init() { // Load plugin files $this->load_files(); // Register hooks $this->register_hooks(); // Initialize components $this->init_components(); // Note: load_plugin_textdomain() is not needed when hosted on WordPress.org // WordPress automatically loads translations for plugins in the directory } /** * Load required files */ private function load_files() { require_once ACOUSTIC_CONNECT_WOO_PLUGIN_DIR . 'includes/class-admin-settings.php'; require_once ACOUSTIC_CONNECT_WOO_PLUGIN_DIR . 'includes/class-event-handler.php'; } /** * Register hooks */ private function register_hooks() { // Enqueue scripts add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); // Add settings link add_filter('plugin_action_links_' . ACOUSTIC_CONNECT_WOO_PLUGIN_BASENAME, array($this, 'add_settings_link')); // AJAX handler for connection test add_action('wp_ajax_acoustic_connect_test_connection', array($this, 'ajax_test_connection')); // Note: Logout handling is done in JavaScript by detecting when customerData.customerId // changes from a value to empty. This approach is more reliable than PHP hooks because // WordPress redirects before wp_footer renders during logout. } /** * Initialize components */ private function init_components() { // Initialize admin settings Acoustic_Connect_Admin_Settings::get_instance(); // Initialize event handler Acoustic_Connect_Event_Handler::get_instance(); } /** * Plugin activation */ public function activate() { // Set default options $defaults = array( 'acoustic_connect_enabled' => 'no', 'acoustic_connect_app_key' => '', 'acoustic_connect_collector_url' => '', 'acoustic_connect_subscription_tier' => 'pro', 'acoustic_connect_sdk_version' => 'latest', 'acoustic_connect_sdk_minified' => 'yes', 'acoustic_connect_debug_mode' => 'no', 'acoustic_connect_track_page_view' => 'yes', 'acoustic_connect_track_product_view' => 'yes', 'acoustic_connect_track_search' => 'yes', 'acoustic_connect_track_add_to_cart' => 'yes', 'acoustic_connect_track_remove_from_cart' => 'yes', 'acoustic_connect_track_checkout' => 'yes', ); foreach ($defaults as $key => $value) { if (false === get_option($key)) { add_option($key, $value); } } // Set activation timestamp add_option('acoustic_connect_activated_time', time()); // Flush rewrite rules flush_rewrite_rules(); } /** * Plugin deactivation */ public function deactivate() { // Flush rewrite rules flush_rewrite_rules(); } /** * Enqueue scripts and styles */ public function enqueue_scripts() { // Only load if enabled and configured $enabled = get_option('acoustic_connect_enabled', 'no'); $app_key = get_option('acoustic_connect_app_key', ''); $collector_url = get_option('acoustic_connect_collector_url', ''); if ('yes' !== $enabled || empty($app_key) || empty($collector_url)) { return; } // Get SDK configuration (CDN is the only source) $sdk_version = get_option('acoustic_connect_sdk_version', 'latest'); $sdk_minified = get_option('acoustic_connect_sdk_minified', 'yes'); // Use CDN for SDK $file_name = ('yes' === $sdk_minified) ? 'acoconnect.min.js' : 'acoconnect.js'; $version_path = ('latest' === $sdk_version) ? 'latest' : $sdk_version; $sdk_url = "https://cdn.goacoustic.com/connect/{$version_path}/{$file_name}"; $sdk_ver = null; // Don't set version for external URLs // Enqueue Acoustic Connect Generic SDK wp_enqueue_script( 'acoustic-connect-sdk', $sdk_url, array(), $sdk_ver, true ); // Get subscription tier $subscription_tier = get_option('acoustic_connect_subscription_tier', 'pro'); $debug_mode = get_option('acoustic_connect_debug_mode', 'no'); // Create dynamic version string that changes when settings change // This ensures browser cache is busted when configuration is updated $wrapper_version = ACOUSTIC_CONNECT_WOO_VERSION . '-' . md5($app_key . $collector_url . $subscription_tier . $debug_mode . $sdk_version . $sdk_minified); // Enqueue WooCommerce wrapper // TODO: consider also having a wrapper in CDN wp_enqueue_script( 'acoustic-connect-wrapper', ACOUSTIC_CONNECT_WOO_PLUGIN_URL . 'assets/js/acoustic-connect-wrapper.js', array('jquery', 'acoustic-connect-sdk'), $wrapper_version, true ); // Pass configuration to JavaScript $config = array( 'appKey' => $app_key, 'postUrl' => $collector_url, 'subscriptionTier' => $subscription_tier, 'debugMode' => ('yes' === $debug_mode), 'currency' => get_woocommerce_currency(), 'customerData' => $this->get_customer_data(), ); // use wp_add_inline_script(string $handle, string $data, string $position = 'after' ): bool // instead of wp_localize_script( string $handle, string $object_name, array $l10n ): bool wp_localize_script('acoustic-connect-wrapper', 'acousticConnectConfig', $config); // Pass product data on product pages if (is_product()) { global $product; if ($product) { $product_data = array( 'id' => $product->get_id(), 'name' => $product->get_name(), 'description' => $product->get_short_description(), 'price' => $product->get_price(), 'regular_price' => $product->get_regular_price(), 'sale_price' => $product->get_sale_price(), 'currency' => get_woocommerce_currency(), 'sku' => $product->get_sku(), 'category' => $this->get_product_category($product), 'url' => get_permalink($product->get_id()), 'image' => wp_get_attachment_url($product->get_image_id()), 'in_stock' => $product->is_in_stock(), 'discount' => $product->get_sale_price() ? ($product->get_regular_price() - $product->get_sale_price()) : 0, ); wp_localize_script('acoustic-connect-wrapper', 'acousticProductData', $product_data); } } // Pass order data on thank-you page if (is_order_received_page()) { $order_id = get_query_var('order-received'); if ($order_id) { $order = wc_get_order($order_id); if ($order) { $order_data = $this->get_order_data($order); wp_localize_script('acoustic-connect-wrapper', 'acousticOrderData', $order_data); } } } } /** * Get product category * * @param WC_Product $product Product object. * @return string */ private function get_product_category($product) { $categories = get_the_terms($product->get_id(), 'product_cat'); if (!empty($categories) && !is_wp_error($categories)) { return $categories[0]->name; } return ''; } /** * Get order data for tracking * * @param WC_Order $order Order object. * @return array */ private function get_order_data($order) { $items = array(); foreach ($order->get_items() as $item) { $product = $item->get_product(); $items[] = array( 'product_id' => $product ? $product->get_id() : 0, 'product_name' => $item->get_name(), 'quantity' => $item->get_quantity(), 'price' => $item->get_total() / $item->get_quantity(), 'discount' => $item->get_subtotal() - $item->get_total(), 'category' => $product ? $this->get_product_category($product) : '', ); } return array( 'order_id' => $order->get_id(), 'total' => $order->get_total(), 'subtotal' => $order->get_subtotal(), 'tax' => $order->get_total_tax(), 'shipping' => $order->get_shipping_total(), 'discount' => $order->get_discount_total(), 'currency' => $order->get_currency(), 'items' => $items, ); } /** * Get customer data for identifiableAttributes * Returns customer ID (WordPress user ID or WC session ID for guests), email, and phone * * @return array */ private function get_customer_data() { $customer_data = array( 'customerId' => '', 'email' => '', 'phone' => '', ); // Try logged-in user ID first if (is_user_logged_in()) { $user_id = get_current_user_id(); $user = wp_get_current_user(); $customer_data['customerId'] = strval($user_id); // Safely get and trim email $email = isset($user->user_email) ? trim($user->user_email) : ''; $customer_data['email'] = !empty($email) ? $email : ''; // Try to get phone from WooCommerce billing data $billing_phone = get_user_meta($user_id, 'billing_phone', true); $phone = is_string($billing_phone) ? trim($billing_phone) : ''; $customer_data['phone'] = !empty($phone) ? $phone : ''; } elseif (function_exists('WC') && WC()->customer) { // For customers: use WooCommerce customer ID $session_customer_id = WC()->customer->get_id(); if ($session_customer_id) { $customer_data['customerId'] = strval($session_customer_id); } } return $customer_data; } /** * AJAX handler for connection test */ public function ajax_test_connection() { // Verify nonce check_ajax_referer('acoustic_connect_admin', 'nonce'); // Check user permissions if (!current_user_can('manage_woocommerce')) { wp_send_json_error(array('message' => 'Unauthorized'), 403); } // Get parameters $collector_url = isset($_POST['collector_url']) ? esc_url_raw(wp_unslash($_POST['collector_url'])) : ''; $app_key = isset($_POST['app_key']) ? sanitize_text_field(wp_unslash($_POST['app_key'])) : ''; if (empty($collector_url)) { wp_send_json_error(array('message' => 'Collector URL is required')); } if (empty($app_key)) { wp_send_json_error(array('message' => 'App Key is required')); } // Create a test signal payload (Type 21 message) $test_payload = array( 'messageVersion' => '13.0.0', 'serialNumber' => 1, 'sessions' => array( array( 'id' => '00000000000000000000000000000001', 'startTime' => round(microtime(true) * 1000), 'timezoneOffset' => 0, 'messages' => array( array( 'type' => 21, 'offset' => 0, 'count' => 1, 'fromWeb' => true, 'signal' => array( 'signalType' => 'test', 'name' => 'Connection test from WordPress plugin', 'category' => 'System', 'effect' => 'positive' ) ) ), 'clientEnvironment' => array( 'webEnvironment' => array( 'libVersion' => '1.0.0-test', 'domain' => wp_parse_url(home_url(), PHP_URL_HOST) ) ) ) ) ); // Send POST request to Collector URL $response = wp_remote_post($collector_url, array( 'timeout' => 10, 'headers' => array( 'Content-Type' => 'application/json', 'X-Tealeaf-SaaS-AppKey' => $app_key, ), 'body' => wp_json_encode($test_payload), 'sslverify' => true )); // Check for errors if (is_wp_error($response)) { wp_send_json_error(array( 'message' => 'Connection failed: ' . $response->get_error_message() )); } $status_code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); // Success responses (200, 201, 204) if ($status_code >= 200 && $status_code < 300) { wp_send_json_success(array( 'message' => 'Connection successful! Status: ' . $status_code, 'status_code' => $status_code )); } // Handle specific error codes if ($status_code === 401 || $status_code === 403) { wp_send_json_error(array( 'message' => 'Authentication failed. Check your App Key. Status: ' . $status_code )); } if ($status_code === 400) { wp_send_json_error(array( 'message' => 'Bad request. The Collector URL may be incorrect. Status: ' . $status_code )); } // Generic error wp_send_json_error(array( 'message' => 'Connection failed with status: ' . $status_code, 'status_code' => $status_code )); } /** * Add settings link to plugins page * * @param array $links Plugin action links. * @return array */ public function add_settings_link($links) { $settings_link = sprintf( '%s', esc_url(admin_url('admin.php?page=wc-settings&tab=acoustic_connect')), esc_html__('Settings', 'acoustic-connect-woo') ); array_unshift($links, $settings_link); return $links; } /** * Declare HPOS compatibility */ public function declare_hpos_compatibility() { if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) { \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', ACOUSTIC_CONNECT_WOO_PLUGIN_FILE, true); } } } /** * Initialize plugin * * @return Acoustic_Connect_Woo */ function acoustic_connect_woo_init() { return Acoustic_Connect_Woo::get_instance(); } // Initialize plugin after all plugins are loaded add_action('plugins_loaded', 'acoustic_connect_woo_init', 10);