$attribute_value ) { // Output the variation attribute values (e.g., "Large / black"). $variant_title .= $attribute_value . ' / '; } // remove the last ' / '. return rtrim( $variant_title, ' / ' ); } /** * Get the WooCommerce cart. * * @param \WC_Cart $cart The WooCommerce cart. * @param string $currency The currency. * @return void */ function aict_get_cart( \WC_Cart $cart, string $currency ): void { $response = new stdClass(); $response->currency = $currency; $response->total = aict_get_numeric_value_from_string( $cart->get_cart_contents_total() ); $response->subtotal = aict_get_numeric_value_from_string( $cart->get_subtotal() ); $response->items = array(); $items = $cart->get_cart_contents(); $product_ids = array(); foreach ( $items as $item ) { // Validate that item is an array and has all required keys. if ( ! is_array( $item ) || ! isset( $item['product_id'], $item['line_subtotal'], $item['line_total'], $item['quantity'] ) ) { continue; } $product_ids[] = $item['product_id']; } $products = \wc_get_products( array( 'include' => $product_ids, ) ); foreach ( $items as $item ) { // Validate that item is an array and has all required keys. if ( ! is_array( $item ) || ! isset( $item['product_id'], $item['line_subtotal'], $item['line_total'], $item['quantity'] ) ) { continue; } $item_product_id = $item['product_id']; $product_index = array_search( $item_product_id, array_map( function ( $product ) { return $product->get_id(); }, $products ), true ); // Skip if product not found in the products array. if ( false === $product_index || ! isset( $products[ $product_index ] ) ) { continue; } $product = $products[ $product_index ]; $subtotal = $item['line_subtotal']; $value = $item['line_total']; $discount = $subtotal - $value; // Safely get variation data with fallback. $variation = $item['variation'] ?? array(); $variation_id = $item['variation_id'] ?? 0; $response->items[] = array( 'id' => $item_product_id, 'thumbnailUrl' => get_the_post_thumbnail_url( $product->get_id(), 'shop_thumbnail' ), 'title' => $product->get_name(), 'variantTitle' => aict_get_variant_title( $variation ), 'variantId' => $variation_id, 'discounts' => array( array( 'amount' => $discount, ), ), 'qty' => $item['quantity'], 'value' => $value, 'productPreviewUrl' => $product->get_permalink(), 'sku' => $product->get_sku(), ); } wp_send_json_success( $response ); } /** * Register WooCommerce sales event hooks. * * @return void */ function aict_register_woocommerce_sales_event_hooks(): void { add_action( 'woocommerce_add_to_cart', 'AICT\aict_capture_added_cart_lines', 10, 4 ); add_action( 'woocommerce_cart_item_removed', 'AICT\aict_capture_removed_cart_lines', 10, 2 ); add_action( 'wp_footer', 'AICT\aict_emit_order_created_event', 15 ); add_action( 'wp_footer', 'AICT\aict_emit_pending_cart_events', 15 ); } /** * Output an inline script on the order confirmation page that emits store_order_was_created. * * Hooked on wp_footer rather than woocommerce_thankyou: WooCommerce's block-based Order * Confirmation page only fires woocommerce_thankyou as a compat shim from inside the * "Additional Information" block, so it never fires on sites whose block template omits * that block. is_order_received_page() reflects the actual endpoint instead, so this runs * regardless of which blocks (or the classic thankyou.php template) render the page. * * @return void */ function aict_emit_order_created_event(): void { if ( ! is_order_received_page() ) return; $order_id = absint( get_query_var( 'order-received' ) ); if ( ! $order_id ) return; $order = wc_get_order( $order_id ); if ( ! $order ) return; // The order key is the access token for guest orders here, mirroring the check // WC_Shortcode_Checkout::order_received() performs before rendering order details. // phpcs:ignore WordPress.Security.NonceVerification.Recommended $order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; if ( '' === $order_key || ! $order->key_is_valid( $order_key ) ) return; // Registered-customer orders must only be reported back to the customer who placed them. $customer_id = $order->get_customer_id(); if ( $customer_id && get_current_user_id() !== $customer_id ) return; $line_items = array(); foreach ( $order->get_items() as $item ) { $product = $item->get_product(); $thumbnail_id = $product ? $product->get_image_id() : 0; $line_items[] = array( 'product_id' => (string) $item->get_product_id(), 'variant_id' => (string) $item->get_variation_id(), 'title' => $item->get_name(), 'quantity' => $item->get_quantity(), 'price' => $item->get_quantity() > 0 ? (float) ( $item->get_subtotal() / $item->get_quantity() ) : 0.0, 'thumbnail_url' => $thumbnail_id ? wp_get_attachment_url( $thumbnail_id ) : null, ); } $order_data = array( 'id' => (string) $order->get_id(), 'order_number' => $order->get_order_number(), 'currency' => $order->get_currency(), 'total_price' => (float) $order->get_total(), 'subtotal_price' => (float) $order->get_subtotal(), 'shipping_price' => (float) $order->get_shipping_total(), 'tax_price' => (float) $order->get_total_tax(), 'status_page_url' => $order->get_view_order_url(), 'line_items' => $line_items, ); $script = sprintf( 'if(window.__aict_text_connect){__aict_text_connect._emit("store_order_was_created",{order:%s},{replayable:true});}', wp_json_encode( $order_data ) ); wp_add_inline_script( 'aict-connect', $script ); } /** * Store just-added cart product data in the WC session for fragment injection. * * @param string $cart_item_key The cart item key. * @param int $product_id The product ID. * @param int $quantity The quantity added. * @param int $variation_id The variation ID (0 for non-variable products). * @return void */ function aict_capture_added_cart_lines( string $cart_item_key, int $product_id, int $quantity, int $variation_id = 0 ): void { if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) return; $product = wc_get_product( $product_id ); if ( ! $product ) return; $thumbnail_id = $product->get_image_id(); $new_line = array( 'total_price' => (float) ( $product->get_price() * $quantity ), 'currency' => get_woocommerce_currency(), 'quantity' => $quantity, 'product' => array( 'id' => (string) $product_id, 'variant_id' => (string) $variation_id, 'title' => $product->get_name(), 'price' => (float) $product->get_price(), 'sku' => $product->get_sku(), 'thumbnail_url' => $thumbnail_id ? wp_get_attachment_url( $thumbnail_id ) : null, ), ); $existing = WC()->session->get( 'aict_added_cart_lines', array() ); WC()->session->set( 'aict_added_cart_lines', array_merge( $existing, array( $new_line ) ) ); } /** * Store just-removed cart product data in the WC session for fragment injection. * * @param string $cart_item_key The cart item key. * @param \WC_Cart $cart The WooCommerce cart. * @return void */ function aict_capture_removed_cart_lines( string $cart_item_key, \WC_Cart $cart ): void { if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) return; $item = $cart->removed_cart_contents[ $cart_item_key ] ?? null; $product = $item ? wc_get_product( $item['product_id'] ) : null; if ( ! $product ) return; $thumbnail_id = $product->get_image_id(); $new_line = array( 'total_price' => (float) ( $item['line_subtotal'] ?? 0 ), 'currency' => get_woocommerce_currency(), 'quantity' => (int) $item['quantity'], 'product' => array( 'id' => (string) $item['product_id'], 'variant_id' => (string) ( $item['variation_id'] ?? 0 ), 'title' => $product->get_name(), 'price' => (float) $product->get_price(), 'sku' => $product->get_sku(), 'thumbnail_url' => $thumbnail_id ? wp_get_attachment_url( $thumbnail_id ) : null, ), ); $existing = WC()->session->get( 'aict_removed_cart_lines', array() ); WC()->session->set( 'aict_removed_cart_lines', array_merge( $existing, array( $new_line ) ) ); } /** * Register inline scripts for pending cart events on page reload (non-AJAX cart). * Hooked at priority 15, before wp_print_footer_scripts at priority 20. * wp_add_inline_script appends to the aict-connect handle, so the script * still runs after textConnect.js is loaded. * * @return void */ function aict_emit_pending_cart_events(): void { if ( ! WC()->session ) { return; } $added = WC()->session->get( 'aict_added_cart_lines' ); $removed = WC()->session->get( 'aict_removed_cart_lines' ); if ( $added ) { wp_add_inline_script( 'aict-connect', sprintf( 'if(window.__aict_text_connect){__aict_text_connect._emit("store_product_added_to_cart",{cart_lines:%s},{replayable:true});}', wp_json_encode( $added ) ) ); WC()->session->__unset( 'aict_added_cart_lines' ); } if ( $removed ) { wp_add_inline_script( 'aict-connect', sprintf( 'if(window.__aict_text_connect){__aict_text_connect._emit("store_product_removed_from_cart",{cart_lines:%s},{replayable:true});}', wp_json_encode( $removed ) ) ); WC()->session->__unset( 'aict_removed_cart_lines' ); } } /** * Refresh cart action for CI. * * @return void */ function aict_refresh_cart_action(): void { $woocommerce = WC(); $cart = $woocommerce->cart; aict_get_cart( $cart, get_woocommerce_currency() ); }