register_hooks(); } /** * Get instance * * @return Acoustic_Connect_Event_Handler */ public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } /** * Register hooks */ private function register_hooks() { // Only register if tracking is enabled if (!$this->is_tracking_enabled()) { return; } // Page context injection add_action('wp_footer', array($this, 'inject_page_context'), 5); // Product view tracking add_action('woocommerce_after_single_product', array($this, 'inject_product_view_tracking')); // Cart events - using proper WooCommerce hooks add_action('woocommerce_add_to_cart', array($this, 'handle_add_to_cart'), 10, 6); add_action('woocommerce_cart_item_removed', array($this, 'handle_remove_from_cart'), 10, 2); // Fragment filters for AJAX cart events - passes data to JavaScript add_filter('woocommerce_add_to_cart_fragments', array($this, 'add_cart_event_fragment'), 10, 1); // Inject product data into cart rows for remove-from-cart tracking add_filter('woocommerce_cart_item_name', array($this, 'inject_cart_item_product_data'), 10, 3); // Checkout events add_action('woocommerce_before_checkout_form', array($this, 'inject_checkout_data')); add_action('woocommerce_checkout_order_processed', array($this, 'handle_order_placed'), 10, 3); // Thank you page add_action('woocommerce_thankyou', array($this, 'inject_order_completion_tracking')); // Search tracking add_action('pre_get_posts', array($this, 'track_search')); // Add product data attributes to product loops add_action('woocommerce_after_shop_loop_item', array($this, 'add_product_data_attributes')); } /** * Check if tracking is enabled * * @return bool */ private function is_tracking_enabled() { $enabled = get_option('acoustic_connect_enabled', 'no'); $app_key = get_option('acoustic_connect_app_key', ''); $collector_url = get_option('acoustic_connect_collector_url', ''); return ('yes' === $enabled && !empty($app_key) && !empty($collector_url)); } /** * Inject page context data */ public function inject_page_context() { $context = array( 'pageType' => $this->get_page_type(), 'timestamp' => time() * 1000, 'userAgent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '', 'language' => get_locale(), ); // Add WooCommerce specific context if (function_exists('WC') && WC()->cart) { $context['cart'] = array( 'itemCount' => WC()->cart->get_cart_contents_count(), 'total' => WC()->cart->get_total('raw'), ); } wp_add_inline_script( 'acoustic-connect-wrapper', 'window.acousticConnectPageContext = ' . wp_json_encode($context) . ';', 'before' ); } /** * Get page type * * @return string */ private function get_page_type() { if (is_front_page()) { return 'home'; } elseif (is_product()) { return 'product'; } elseif (is_product_category()) { return 'category'; } elseif (is_product_tag()) { return 'tag'; } elseif (is_shop()) { return 'shop'; } elseif (is_cart()) { return 'cart'; } elseif (is_checkout()) { return 'checkout'; } elseif (is_account_page()) { return 'account'; } elseif (is_search()) { return 'search'; } elseif (is_page()) { return 'page'; } return 'unknown'; } /** * Inject product view tracking */ public function inject_product_view_tracking() { if ('yes' !== get_option('acoustic_connect_track_product_view', 'yes')) { return; } global $product; if (!$product) { return; } $product_data = $this->get_product_data($product); wp_add_inline_script( 'acoustic-connect-wrapper', 'window.acousticProductData = ' . wp_json_encode($product_data) . ';', 'before' ); } /** * Handle add to cart * * Stores event data in WC session for the fragment filter to pick up. * This data is then passed to JavaScript via AJAX fragments. * * @param string $cart_item_key Cart item key. * @param int $product_id Product ID. * @param int $quantity Quantity. * @param int $variation_id Variation ID. * @param array $variation Variation data. * @param array $cart_item_data Cart item data. */ public function handle_add_to_cart($cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data) { if ('yes' !== get_option('acoustic_connect_track_add_to_cart', 'yes')) { return; } $product = wc_get_product($variation_id ? $variation_id : $product_id); if (!$product) { return; } // Store event in WC session for fragment filter to pick up (same request) $event_data = array( 'type' => 'addToCart', 'product' => $this->get_product_data($product), 'quantity' => $quantity, 'cart_item_key' => $cart_item_key, ); // Use WC session instead of transients - more reliable for same-request data passing if (function_exists('WC') && WC()->session) { WC()->session->set('acoustic_last_cart_event', $event_data); } } /** * Add cart event data to AJAX fragments * * This is called after add-to-cart AJAX and passes event data to JavaScript * via the fragments response. * * @param array $fragments Cart fragments. * @return array Modified fragments. */ public function add_cart_event_fragment($fragments) { if (!function_exists('WC') || !WC()->session) { return $fragments; } $event_data = WC()->session->get('acoustic_last_cart_event'); if ($event_data) { // Clear after reading to avoid duplicate events WC()->session->set('acoustic_last_cart_event', null); // Add as a script tag that JavaScript can read from fragments $fragments['.acoustic-cart-event'] = ''; } return $fragments; } /** * Inject product data into cart item name * * Adds hidden product data span to cart rows for remove-from-cart tracking. * * @param string $product_name Product name HTML. * @param array $cart_item Cart item data. * @param string $cart_item_key Cart item key. * @return string Modified product name HTML. */ public function inject_cart_item_product_data($product_name, $cart_item, $cart_item_key) { $product = $cart_item['data'] ?? null; if (!$product) { return $product_name; } $product_data = $this->get_product_data($product); $product_data['quantity'] = $cart_item['quantity']; $product_data['cart_item_key'] = $cart_item_key; $data_span = ''; return $product_name . $data_span; } /** * Handle remove from cart * * Note: This hook fires AFTER the item is removed, so we need to get the item * from removed_cart_contents, not from get_cart_item(). * * @param string $cart_item_key Cart item key. * @param WC_Cart $cart Cart object. */ public function handle_remove_from_cart($cart_item_key, $cart) { if ('yes' !== get_option('acoustic_connect_track_remove_from_cart', 'yes')) { return; } // Item has already been removed, check removed_cart_contents $cart_item = $cart->removed_cart_contents[$cart_item_key] ?? null; if (!$cart_item) { return; } $product = wc_get_product($cart_item['product_id']); if (!$product) { return; } // Store event in WC session for potential AJAX fragment response $event_data = array( 'type' => 'removeFromCart', 'product' => $this->get_product_data($product), 'quantity' => $cart_item['quantity'], ); // Use WC session instead of transients if (function_exists('WC') && WC()->session) { WC()->session->set('acoustic_last_remove_event', $event_data); } } /** * Inject checkout data */ public function inject_checkout_data() { if ('yes' !== get_option('acoustic_connect_track_checkout', 'yes')) { return; } $cart_data = $this->get_cart_data(); wp_add_inline_script( 'acoustic-connect-wrapper', 'window.acousticConnectCartData = ' . wp_json_encode($cart_data) . ';', 'before' ); } /** * Handle order placed * * @param int $order_id Order ID. * @param array $posted_data Posted data. * @param WC_Order $order Order object. */ public function handle_order_placed($order_id, $posted_data, $order) { if ('yes' !== get_option('acoustic_connect_track_checkout', 'yes')) { return; } $order_data = array( 'order_id' => $order_id, 'cart_value' => $order->get_total(), 'subtotal' => $order->get_subtotal(), 'shipping_cost' => $order->get_shipping_total(), 'tax' => $order->get_total_tax(), 'currency' => $order->get_currency(), 'payment_method' => $order->get_payment_method(), 'line_items' => array(), ); foreach ($order->get_items() as $item) { $product = $item->get_product(); if ($product) { $quantity = max(1, $item->get_quantity()); $order_data['line_items'][] = array( 'product_id' => $product->get_id(), 'product_name' => $product->get_name(), 'quantity' => $quantity, 'base_price' => $product->get_price(), 'purchase_price' => $item->get_total() / $quantity, ); } } // Store in transient for thank you page set_transient('acoustic_order_' . $order_id, $order_data, 3600); } /** * Inject order completion tracking on thank you page * * @param int $order_id Order ID. */ public function inject_order_completion_tracking($order_id) { if (!$order_id) { return; } $order_data = get_transient('acoustic_order_' . $order_id); if (!$order_data) { return; } wp_add_inline_script( 'acoustic-connect-wrapper', 'window.acousticOrderData = ' . wp_json_encode($order_data) . ';', 'before' ); // Delete transient delete_transient('acoustic_order_' . $order_id); } /** * Track search * * Note: Search tracking is now handled entirely by JavaScript which extracts * the search term from URL parameters and counts results from the DOM. * This method is kept for potential future server-side enhancements. * * @param WP_Query $query Query object. */ public function track_search($query) { // Search tracking is handled client-side in acoustic-connect-wrapper.js // This hook is kept for potential future server-side search analytics return; } /** * Add product data attributes */ public function add_product_data_attributes() { global $product; if (!$product) { return; } $product_data = $this->get_product_data($product); echo ''; } /** * Get product data * * @param WC_Product $product Product object. * @return array */ private function get_product_data($product) { $categories = array(); $terms = get_the_terms($product->get_id(), 'product_cat'); if ($terms && !is_wp_error($terms)) { foreach ($terms as $term) { $categories[] = $term->name; } } $data = array( 'product_id' => $product->get_id(), 'product_name' => $product->get_name(), 'sale_price' => $product->get_price(), 'regular_price' => $product->get_regular_price(), 'discount_price' => $product->get_sale_price(), 'currency' => get_woocommerce_currency(), 'sku' => $product->get_sku(), 'category_names' => $categories, 'in_stock' => $product->is_in_stock(), 'product_type' => $product->get_type(), 'product_url' => $product->get_permalink(), ); return apply_filters('acoustic_connect_product_data', $data, $product); } /** * Get cart data * * @return array */ private function get_cart_data() { if (!function_exists('WC') || !WC()->cart) { return array(); } $items = array(); foreach (WC()->cart->get_cart() as $cart_item) { $product = $cart_item['data']; if ($product) { $items[] = array( 'id' => $product->get_id(), 'name' => $product->get_name(), 'quantity' => $cart_item['quantity'], 'price' => $product->get_price(), 'total' => $cart_item['line_total'], ); } } $data = array( 'items' => $items, 'itemCount' => WC()->cart->get_cart_contents_count(), 'subtotal' => WC()->cart->get_cart_subtotal(), 'total' => WC()->cart->get_total('raw'), 'currency' => get_woocommerce_currency(), ); return apply_filters('acoustic_connect_cart_data', $data); } }