diff --git a/admin/admin-tab-integrate.php b/admin/admin-tab-integrate.php index 185f1f5..bc1952a 100644 --- a/admin/admin-tab-integrate.php +++ b/admin/admin-tab-integrate.php @@ -36,6 +36,25 @@ 'phase' => GTM4WP_PHASE_STABLE, 'plugintocheck' => 'woocommerce/woocommerce.php', ), + GTM4WP_OPTION_INTEGRATE_WCBLOCKSADDTOCART => array( + 'label' => esc_html__( 'Use WooCommerce Blocks add-to-cart', 'duracelltomi-google-tag-manager' ), + 'description' => sprintf( + gtm4wp_safe_admin_html( + // translators: 1: anchor element linking to WooCommerce Blocks DOM events docs. 2: closing anchor element. + __( + 'Enable this experimental feature to track WooCommerce Blocks cart interactions (Add to Cart + Options, mini-cart, and cart page).
+ This option uses the %1$sDOM events%2$s from WooCommerce Blocks to detect when products are added to or removed from the cart.
+ When enabled, the legacy add-to-cart tracking will be disabled.
+ This feature requires "Track e-commerce" to be enabled.', + 'duracelltomi-google-tag-manager' + ) + ), + '', + '' + ), + 'phase' => GTM4WP_PHASE_EXPERIMENTAL, + 'plugintocheck' => 'woocommerce/woocommerce.php', + ), GTM4WP_OPTION_INTEGRATE_WCPRODPERIMPRESSION => array( 'label' => esc_html__( 'Products per impression', 'duracelltomi-google-tag-manager' ), 'description' => gtm4wp_safe_admin_html( diff --git a/admin/admin.php b/admin/admin.php index 5b23997..112379f 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -652,8 +652,13 @@ function gtm4wp_sanitize_options( $options ) { } } elseif ( GTM4WP_OPTION_GTM_PLACEMENT === $optionname ) { // GTM container ON/OFF + compat mode. - $container_on_off = (bool) $options['container-on']; - $container_compat = (int) $options['compat-mode']; + $container_on_off = isset( $options['container-on'] ) + ? (bool) $options['container-on'] + : ( GTM4WP_PLACEMENT_OFF !== (int) $optionvalue ); + + $container_compat = isset( $options['compat-mode'] ) + ? (int) $options['compat-mode'] + : (int) $optionvalue; if ( ! $container_on_off ) { $output[ $optionname ] = GTM4WP_PLACEMENT_OFF; @@ -1298,6 +1303,19 @@ function gtm4wp_show_warning() { ); echo '

'; } + + if ( $gtm4wp_options[ GTM4WP_OPTION_INTEGRATE_WCTRACKECOMMERCE ] && $gtm4wp_options[ GTM4WP_OPTION_INTEGRATE_WCBLOCKSADDTOCART ] ) { + $blocks_health = gtm4wp_get_blocks_integration_health_state(); + + if ( ! $blocks_health['store_api_route'] || ! $blocks_health['gtm4wp_product_api'] ) { + echo '

'; + esc_html_e( + 'WooCommerce Blocks add-to-cart tracking is enabled but its required REST endpoints are unavailable. Please ensure WooCommerce Blocks assets and the GTM4WP REST endpoints are accessible.', + 'duracelltomi-google-tag-manager' + ); + echo '

'; + } + } } /** @@ -1381,6 +1399,82 @@ function gtm4wp_show_upgrade_notification( $current_plugin_metadata, $new_plugin add_action( 'admin_menu', 'gtm4wp_add_admin_page' ); add_action( 'admin_enqueue_scripts', 'gtm4wp_add_admin_js' ); add_action( 'admin_notices', 'gtm4wp_show_warning' ); + +/** + * Returns health status for WooCommerce Blocks add-to-cart integration. + * + * @return array + */ +function gtm4wp_get_blocks_integration_health_state() { + $result = array( + 'store_api_route' => false, + 'gtm4wp_product_api' => false, + ); + + // First, try to exercise the endpoints directly so themes/hosts that lazy-load routes do not trigger false warnings. + $result['store_api_route'] = gtm4wp_rest_route_responds( '/wc/store/v1/cart', true ); + $result['gtm4wp_product_api'] = gtm4wp_rest_route_responds( '/gtm4wp/v1/product/0', true ); + + if ( $result['store_api_route'] && $result['gtm4wp_product_api'] ) { + return $result; + } + + if ( ! function_exists( 'rest_get_server' ) ) { + return $result; + } + + $server = rest_get_server(); + + if ( ! $server ) { + return $result; + } + + $routes = $server->get_routes(); + + foreach ( $routes as $route => $handler ) { + if ( ! $result['store_api_route'] && 0 === strpos( $route, '/wc/store/v1/cart' ) ) { + $result['store_api_route'] = true; + } + + if ( ! $result['gtm4wp_product_api'] && 0 === strpos( $route, '/gtm4wp/v1/product' ) ) { + $result['gtm4wp_product_api'] = true; + } + + if ( $result['store_api_route'] && $result['gtm4wp_product_api'] ) { + break; + } + } + + return $result; +} + +/** + * Checks if a REST route responds without returning rest_no_route. + * + * @param string $route Route path to check. + * @param bool $allow_client_error Treat 4xx responses as success (route exists but request invalid). + * @return bool + */ +function gtm4wp_rest_route_responds( $route, $allow_client_error = false ) { + if ( ! class_exists( 'WP_REST_Request' ) || ! function_exists( 'rest_do_request' ) ) { + return false; + } + + $request = new WP_REST_Request( 'GET', $route ); + $response = rest_do_request( $request ); + + if ( is_wp_error( $response ) ) { + return 'rest_no_route' !== $response->get_error_code(); + } + + $status = (int) $response->get_status(); + + if ( $allow_client_error ) { + return $status >= 200 && $status < 500; + } + + return $status >= 200 && $status < 400; +} add_action( 'admin_head', 'gtm4wp_admin_head' ); add_filter( 'plugin_action_links', 'gtm4wp_add_plugin_action_links', 10, 2 ); add_action( 'wp_ajax_gtm4wp_dismiss_notice', 'gtm4wp_dismiss_notice' ); diff --git a/common/readoptions.php b/common/readoptions.php index e2cef86..9d66483 100644 --- a/common/readoptions.php +++ b/common/readoptions.php @@ -90,6 +90,7 @@ define( 'GTM4WP_OPTION_INTEGRATE_WCNOORDERTRACKEDFLAG', 'integrate-woocommerce-do-not-use-order-tracked-flag' ); define( 'GTM4WP_OPTION_INTEGRATE_WCCLEARECOMMERCEDL', 'integrate-woocommerce-clear-ecommerce-datalayer' ); define( 'GTM4WP_OPTION_INTEGRATE_WCDLMAXTIMEOUT', 'integrate-woocommerce-datalayer-max-timeout' ); +define( 'GTM4WP_OPTION_INTEGRATE_WCBLOCKSADDTOCART', 'integrate-woocommerce-blocks-add-to-cart' ); define( 'GTM4WP_OPTION_INTEGRATE_WPECOMMERCE', 'integrate-wp-e-commerce' ); @@ -199,6 +200,7 @@ GTM4WP_OPTION_INTEGRATE_WCNOORDERTRACKEDFLAG => false, GTM4WP_OPTION_INTEGRATE_WCCLEARECOMMERCEDL => false, GTM4WP_OPTION_INTEGRATE_WCDLMAXTIMEOUT => 2000, + GTM4WP_OPTION_INTEGRATE_WCBLOCKSADDTOCART => false, GTM4WP_OPTION_INTEGRATE_WPECOMMERCE => false, diff --git a/dist/js/gtm4wp-woocommerce.js b/dist/js/gtm4wp-woocommerce.js index 3fd8988..f6ae116 100644 --- a/dist/js/gtm4wp-woocommerce.js +++ b/dist/js/gtm4wp-woocommerce.js @@ -1 +1 @@ -"use strict";var gtm4wp_last_selected_product_variation;function gtm4wp_woocommerce_handle_cart_qty_change(){document.querySelectorAll(".product-quantity input.qty").forEach(function(t){var e=t.defaultValue,o=parseInt(t.value);if(e!=(o=isNaN(o)?e:o)){var t=t.closest(".cart_item"),t=t&&t.querySelector(".remove");if(t)return!(t=gtm4wp_read_json_from_node(t,"gtm4wp_product_data"))||void(ediv:not(.product-category) a:not(.add_to_cart_button):not(.quick-view-button),.widget-product-item,.woocommerce-grouped-product-list-item__label a")){if("undefined"==typeof google_tag_manager)return!0;c=t.target,_=c.closest(".products li:not(.product-category) a:not(.add_to_cart_button):not(.quick-view-button),.wc-block-grid__products li:not(.product-category) a:not(.add_to_cart_button):not(.quick-view-button),.products>div:not(.product-category) a:not(.add_to_cart_button):not(.quick-view-button),.widget-product-item,.woocommerce-grouped-product-list-item__label a");if(!_)return!0;var i,r=c.closest(".product,.wc-block-grid__product"),o=(r=(r=r||((r=c.closest(".products li"))||c.closest(".products>div")))||c.closest(".woocommerce-grouped-product-list-item__label"))?r.querySelector(".gtm4wp_productdata"):c,e=gtm4wp_read_json_from_node(o,"gtm4wp_product_data",["internal_id"]);if(!e)return!0;if(e.productlink!=_.getAttribute("href"))return!0;for(i in window.google_tag_manager)if("gtm-"==i.substring(0,4).toLowerCase()){window.gtm4wp_first_container_id=i;break}if(""===window.gtm4wp_first_container_id)return!0;var d=t.ctrlKey||t.metaKey,u="_blank"===_.target,p=t.defaultPrevented,m=(p||t.preventDefault(),(d||u)&&(window.productpage_window=window.open("about:blank","_blank")),e.productlink),r=(delete e.productlink,2e3);window.gtm4wp_datalayer_max_timeout&&(r=window.gtm4wp_datalayer_max_timeout),gtm4wp_push_ecommerce("select_item",[e],{currency:gtm4wp_currency},function(t){if(void 0!==t&&window.gtm4wp_first_container_id!=t)return!0;p||((u||d)&&productpage_window?productpage_window.location.href=m:document.location.href=m)},r)}},{capture:!0}),jQuery(document).on("found_variation",function(t,e){if(void 0!==e&&("interactive"!==document.readyState||!gtm4wp_view_item_fired_during_pageload)){t=t.target;if(!t)return!0;var o,t=t.querySelector("[name=gtm4wp_product_data]");if(!t)return!0;try{o=JSON.parse(t.value)}catch(t){return console&&console.error&&console.error(t.message),!0}o.price=gtm4wp_make_sure_is_float(o.price),o.item_group_id=o.id,o.id=e.variation_id,o.item_id=e.variation_id,o.sku=e.sku,gtm4wp_use_sku_instead&&e.sku&&""!==e.sku&&(o.id=e.sku,o.item_id=e.sku),o.price=gtm4wp_make_sure_is_float(e.display_price);var r,c=[];for(r in e.attributes)c.push(e.attributes[r]);o.item_variant=c.join(","),delete(gtm4wp_last_selected_product_variation=o).internal_id,gtm4wp_push_ecommerce("view_item",[o],{currency:gtm4wp_currency,value:o.price}),"interactive"===document.readyState&&(gtm4wp_view_item_fired_during_pageload=!0)}}),jQuery(".variations select").trigger("change"),jQuery(document).ajaxSuccess(function(t,e,o){void 0!==o&&-1div:not(.product-category) a:not(.add_to_cart_button):not(.quick-view-button),.widget-product-item,.woocommerce-grouped-product-list-item__label a")){if("undefined"==typeof google_tag_manager)return!0;r=t.target,a=r.closest(".products li:not(.product-category) a:not(.add_to_cart_button):not(.quick-view-button),.wc-block-grid__products li:not(.product-category) a:not(.add_to_cart_button):not(.quick-view-button),.products>div:not(.product-category) a:not(.add_to_cart_button):not(.quick-view-button),.widget-product-item,.woocommerce-grouped-product-list-item__label a");if(!a)return!0;var i,o=r.closest(".product,.wc-block-grid__product"),n=(o=(o=o||((o=r.closest(".products li"))||r.closest(".products>div")))||r.closest(".woocommerce-grouped-product-list-item__label"))?o.querySelector(".gtm4wp_productdata"):r,e=gtm4wp_read_json_from_node(n,"gtm4wp_product_data",["internal_id"]);if(!e)return!0;if(e.productlink!=a.getAttribute("href"))return!0;for(i in window.google_tag_manager)if("gtm-"==i.substring(0,4).toLowerCase()){window.gtm4wp_first_container_id=i;break}if(""===window.gtm4wp_first_container_id)return!0;var c=t.ctrlKey||t.metaKey,_="_blank"===a.target,s=t.defaultPrevented,p=(s||t.preventDefault(),(c||_)&&(window.productpage_window=window.open("about:blank","_blank")),e.productlink),o=(delete e.productlink,2e3);window.gtm4wp_datalayer_max_timeout&&(o=window.gtm4wp_datalayer_max_timeout),gtm4wp_push_ecommerce("select_item",[e],{currency:gtm4wp_currency},function(t){if(void 0!==t&&window.gtm4wp_first_container_id!=t)return!0;s||((_||c)&&productpage_window?productpage_window.location.href=p:document.location.href=p)},o)}},{capture:!0}),gtm4wp_blocks_integration_enabled||document.addEventListener("click",gtm4wp_classic_add_to_cart_click_handler,{capture:!0}),jQuery(document).on("found_variation",function(t,e){if(void 0!==e&&("interactive"!==document.readyState||!gtm4wp_view_item_fired_during_pageload)){t=t.target;if(!t)return!0;var r,t=t.querySelector("[name=gtm4wp_product_data]");if(!t)return!0;try{r=JSON.parse(t.value)}catch(t){return console&&console.error&&console.error(t.message),!0}r.price=gtm4wp_make_sure_is_float(r.price),r.item_group_id=r.id,r.id=e.variation_id,r.item_id=e.variation_id,r.sku=e.sku,gtm4wp_use_sku_instead&&e.sku&&""!==e.sku&&(r.id=e.sku,r.item_id=e.sku),r.price=gtm4wp_make_sure_is_float(e.display_price);var n,o=[];for(n in e.attributes)o.push(e.attributes[n]);r.item_variant=o.join(","),delete(gtm4wp_last_selected_product_variation=r).internal_id,gtm4wp_push_ecommerce("view_item",[r],{currency:gtm4wp_currency,value:r.price}),"interactive"===document.readyState&&(gtm4wp_view_item_fired_during_pageload=!0)}}),jQuery(".variations select").trigger("change"),jQuery(document).ajaxSuccess(function(t,e,r){void 0!==r&&-1get_sku() ) { + $parent_item_id = $parent_product->get_sku(); + } + + $_temp_productdata['item_group_sku'] = $parent_item_id; + + if ( $gtm4wp_options[ GTM4WP_OPTION_INTEGRATE_WCUSESKU ] && '' !== $parent_item_id ) { + $_temp_productdata['item_group_id'] = $parent_item_id; + } else { + $_temp_productdata['item_group_id'] = $parent_product_id; + } } if ( 1 === count( $product_cat_parts ) ) { @@ -962,7 +981,8 @@ function gtm4wp_woocommerce_single_add_to_cart_tracking() { global $product, $gtm4wp_options; // exit early if there is nothing to do. - if ( false === $gtm4wp_options[ GTM4WP_OPTION_INTEGRATE_WCTRACKECOMMERCE ] ) { + if ( false === $gtm4wp_options[ GTM4WP_OPTION_INTEGRATE_WCTRACKECOMMERCE ] + || ! empty( $gtm4wp_options[ GTM4WP_OPTION_INTEGRATE_WCBLOCKSADDTOCART ] ) ) { return; } @@ -1376,8 +1396,27 @@ function gtm4wp_woocommerce_enqueue_scripts() { if ( $gtm4wp_options[ GTM4WP_OPTION_INTEGRATE_WCTRACKECOMMERCE ] ) { $in_footer = (bool) apply_filters( 'gtm4wp_' . GTM4WP_OPTION_INTEGRATE_WCTRACKECOMMERCE, true ); - wp_enqueue_script( 'gtm4wp-ecommerce-generic', $gtp4wp_script_path . 'gtm4wp-ecommerce-generic.js', array(), GTM4WP_VERSION, $in_footer ); - wp_enqueue_script( 'gtm4wp-woocommerce', $gtp4wp_script_path . 'gtm4wp-woocommerce.js', array( 'jquery' ), GTM4WP_VERSION, $in_footer ); + + $default_version = GTM4WP_VERSION; + $generic_script_ver = $default_version; + $woocommerce_script_ver = $default_version; + + if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { + $script_dir = trailingslashit( dirname( __DIR__ ) ) . 'js/'; + + $generic_script_path = $script_dir . 'gtm4wp-ecommerce-generic.js'; + if ( file_exists( $generic_script_path ) ) { + $generic_script_ver = filemtime( $generic_script_path ); + } + + $woocommerce_script_path = $script_dir . 'gtm4wp-woocommerce.js'; + if ( file_exists( $woocommerce_script_path ) ) { + $woocommerce_script_ver = filemtime( $woocommerce_script_path ); + } + } + + wp_enqueue_script( 'gtm4wp-ecommerce-generic', $gtp4wp_script_path . 'gtm4wp-ecommerce-generic.js', array(), $generic_script_ver, $in_footer ); + wp_enqueue_script( 'gtm4wp-woocommerce', $gtp4wp_script_path . 'gtm4wp-woocommerce.js', array( 'jquery' ), $woocommerce_script_ver, $in_footer ); } } @@ -1507,6 +1546,107 @@ function gtm4wp_woocommerce_add_productdata_to_wc_block( $content, $data, $produ add_action( 'wp_enqueue_scripts', 'gtm4wp_woocommerce_enqueue_scripts' ); add_filter( GTM4WP_WPFILTER_ADDGLOBALVARS_ARRAY, 'gtm4wp_woocommerce_add_global_vars' ); +add_action( 'rest_api_init', 'gtm4wp_register_wc_product_rest_route' ); +add_action( 'wp_ajax_gtm4wp_product_data', 'gtm4wp_ajax_get_product_data' ); +add_action( 'wp_ajax_nopriv_gtm4wp_product_data', 'gtm4wp_ajax_get_product_data' ); + +/** + * Registers REST API endpoint to fetch processed product data. + * + * @return void + */ +function gtm4wp_register_wc_product_rest_route() { + register_rest_route( + 'gtm4wp/v1', + '/product/(?P\d+)', + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => 'gtm4wp_rest_get_product_data', + 'permission_callback' => '__return_true', + ) + ); +} + +/** + * REST API callback to retrieve processed product data. + * + * @param WP_REST_Request $request The REST request. + * @return WP_REST_Response|WP_Error + */ +function gtm4wp_rest_get_product_data( WP_REST_Request $request ) { + $product_id = absint( $request['id'] ); + + if ( $product_id <= 0 ) { + return new WP_Error( 'gtm4wp_invalid_product', __( 'Invalid product ID.', 'duracelltomi-google-tag-manager' ), array( 'status' => 400 ) ); + } + + $product = wc_get_product( $product_id ); + + if ( ! $product ) { + return new WP_Error( 'gtm4wp_product_not_found', __( 'Product not found.', 'duracelltomi-google-tag-manager' ), array( 'status' => 404 ) ); + } + + $product_data = gtm4wp_woocommerce_process_product( $product, array(), 'productdetail' ); + + if ( ! $product_data ) { + return new WP_Error( 'gtm4wp_product_unavailable', __( 'Unable to build product data.', 'duracelltomi-google-tag-manager' ), array( 'status' => 404 ) ); + } + + return rest_ensure_response( + array( + 'product' => $product_data, + ) + ); +} + +/** + * AJAX fallback handler for product data when REST is not accessible. + * + * @return void + */ +function gtm4wp_ajax_get_product_data() { + check_ajax_referer( 'gtm4wp-product-data', 'nonce' ); + + $product_id = isset( $_REQUEST['product_id'] ) ? absint( $_REQUEST['product_id'] ) : 0; + + if ( $product_id <= 0 ) { + wp_send_json_error( + array( + 'message' => __( 'Invalid product ID.', 'duracelltomi-google-tag-manager' ), + ), + 400 + ); + } + + $product = wc_get_product( $product_id ); + + if ( ! $product ) { + wp_send_json_error( + array( + 'message' => __( 'Product not found.', 'duracelltomi-google-tag-manager' ), + ), + 404 + ); + } + + $product_data = gtm4wp_woocommerce_process_product( $product, array(), 'ajax_fallback' ); + + if ( ! $product_data ) { + wp_send_json_error( + array( + 'message' => __( 'Unable to build product data.', 'duracelltomi-google-tag-manager' ), + ), + 500 + ); + } + + wp_send_json_success( + array( + 'product' => $product_data, + ) + ); +} + add_filter( 'woocommerce_blocks_product_grid_item_html', 'gtm4wp_woocommerce_add_productdata_to_wc_block', 10, 3 ); add_action( 'woocommerce_thankyou', 'gtm4wp_woocommerce_thankyou' ); diff --git a/js/gtm4wp-woocommerce.js b/js/gtm4wp-woocommerce.js index 1643316..36641ec 100644 --- a/js/gtm4wp-woocommerce.js +++ b/js/gtm4wp-woocommerce.js @@ -4,6 +4,242 @@ window.gtm4wp_view_item_fired_during_pageload = false; window.gtm4wp_checkout_step_fired = []; // step 1 will be the billing section which is reported during pageload, no need to handle here window.gtm4wp_first_container_id = ""; +const gtm4wp_blocks_integration_enabled = ( typeof gtm4wp_blocks_add_to_cart !== 'undefined' && gtm4wp_blocks_add_to_cart ); +const gtm4wp_rest_root_url = ( () => { + if ( typeof gtm4wp_rest_root !== 'undefined' && gtm4wp_rest_root ) { + return gtm4wp_rest_root; + } + + if ( window.wpApiSettings && window.wpApiSettings.root ) { + return window.wpApiSettings.root; + } + + return window.location.origin.replace( /\/$/, '' ) + '/wp-json/'; +} )(); +const gtm4wp_rest_nonce_value = ( typeof gtm4wp_rest_nonce !== 'undefined' && gtm4wp_rest_nonce ) ? + gtm4wp_rest_nonce : + ( window.wpApiSettings && window.wpApiSettings.nonce ? window.wpApiSettings.nonce : null ); +const gtm4wp_blocks_ajax_endpoint = ( typeof gtm4wp_blocks_ajax_url !== 'undefined' && gtm4wp_blocks_ajax_url ) ? + gtm4wp_blocks_ajax_url : + ( typeof ajaxurl !== 'undefined' ? ajaxurl : '/wp-admin/admin-ajax.php' ); +const gtm4wp_blocks_product_nonce_value = ( typeof gtm4wp_blocks_product_nonce !== 'undefined' && gtm4wp_blocks_product_nonce ) ? + gtm4wp_blocks_product_nonce : + null; +const gtm4wp_blocks_dedupe_window_value = ( () => { + const configured = ( typeof gtm4wp_blocks_dedupe_window !== 'undefined' ) ? parseInt( gtm4wp_blocks_dedupe_window, 10 ) : NaN; + return Number.isFinite( configured ) && configured > 0 ? configured : 800; +} )(); +let gtm4wp_blocks_environment_warned = false; +const gtm4wp_blocks_cart_storage_key = 'gtm4wp_wc_blocks_cart_state'; +const gtm4wp_blocks_cart_storage_version = 1; +let gtm4wp_session_storage_supported = null; + +function gtm4wp_build_rest_url( path ) { + const sanitizedRoot = gtm4wp_rest_root_url.replace( /\/+$/, '' ); + const sanitizedPath = String( path || '' ).replace( /^\/+/, '' ); + return sanitizedRoot + '/' + sanitizedPath; +} + +function gtm4wp_blocks_warn_once( message ) { + if ( gtm4wp_blocks_environment_warned ) { + return; + } + + gtm4wp_blocks_environment_warned = true; + + if ( window.console && window.console.warn ) { + window.console.warn( '[GTM4WP][WC Blocks]', message ); + } +} + +function gtm4wp_is_blocks_store_available() { + return !! ( window.wp && window.wp.data && window.wp.data.select && window.wp.data.subscribe ); +} + +function gtm4wp_delay( duration ) { + return new Promise( ( resolve ) => { + window.setTimeout( resolve, duration ); + } ); +} + +function gtm4wp_is_session_storage_available() { + if ( null !== gtm4wp_session_storage_supported ) { + return gtm4wp_session_storage_supported; + } + + try { + const test_key = '__gtm4wp_ss__'; + window.sessionStorage.setItem( test_key, '1' ); + window.sessionStorage.removeItem( test_key ); + gtm4wp_session_storage_supported = true; + } catch ( e ) { + gtm4wp_session_storage_supported = false; + } + + return gtm4wp_session_storage_supported; +} + +function gtm4wp_normalize_key_value_pairs( source ) { + if ( ! source ) { + return ''; + } + + const entries = []; + + if ( Array.isArray( source ) ) { + source.forEach( ( item ) => { + if ( item && 'object' === typeof item ) { + const name = item.name || item.attribute || item.key || ''; + const value = ( 'value' in item ) ? item.value : ( item.value_html || item.label || '' ); + + if ( name || value ) { + entries.push( name + ':' + value ); + } + } + } ); + } else if ( 'object' === typeof source ) { + Object.keys( source ) + .sort() + .forEach( ( key ) => { + entries.push( key + ':' + source[ key ] ); + } ); + } + + return entries.join( '|' ); +} + +function gtm4wp_classic_add_to_cart_click_handler( e ) { + let event_target_element = e.target; + + if ( !event_target_element ) { + return true; + } + + // track add to cart events for simple products in product lists. + if ( event_target_element.closest( '.add_to_cart_button:not(.product_type_variable, .product_type_grouped, .single_add_to_cart_button)' ) ) { + const product_el = event_target_element.closest( '.product,.wc-block-grid__product' ); + + const productdata_el = product_el && product_el.querySelector( '.gtm4wp_productdata' ); + if ( !productdata_el ) { + return true; + } + + const productdata = gtm4wp_read_json_from_node( productdata_el, "gtm4wp_product_data" ); + if ( !productdata ) { + return true; + } + + if ( "variable" === productdata.product_type || "grouped" === productdata.product_type ) { + return true; + } + + if ( productdata.productlink ) { + delete productdata.productlink; + } + delete productdata.product_type; + productdata.quantity = 1; + + gtm4wp_push_ecommerce( 'add_to_cart', [ productdata ], { + 'currency': gtm4wp_currency, + 'value': productdata.price + }); + + return true; + } + + // track add to cart events for products on product detail pages. + const add_to_cart_button = event_target_element.closest( '.single_add_to_cart_button' ); + if ( !add_to_cart_button ) { + return true; + } + + if ( add_to_cart_button.classList.contains( 'disabled' ) || add_to_cart_button.disabled ) { + // do not track clicks on disabled buttons. + return true; + } + + const product_form = event_target_element.closest( 'form.cart' ); + if ( !product_form ) { + return true; + } + + let product_variant_id = product_form.querySelectorAll( '[name=variation_id]' ); + let product_is_grouped = product_form.classList && product_form.classList.contains( 'grouped_form' ); + + if ( product_variant_id.length > 0 ) { + if ( gtm4wp_last_selected_product_variation ) { + const qty_el = product_form.querySelector( '[name=quantity]' ); + gtm4wp_last_selected_product_variation.quantity = (qty_el && qty_el.value) || 1; + + gtm4wp_push_ecommerce( 'add_to_cart', [ gtm4wp_last_selected_product_variation ], { + 'currency': gtm4wp_currency, + 'value': (gtm4wp_last_selected_product_variation.price * gtm4wp_last_selected_product_variation.quantity).toFixed(2) + }); + } + + return true; + } + + if ( product_is_grouped ) { + const products_in_group = document.querySelectorAll( '.grouped_form .gtm4wp_productdata' ); + let products = []; + let sum_value = 0; + + products_in_group.forEach( function( product_data_el ) { + const productdata = gtm4wp_read_json_from_node(product_data_el, 'gtm4wp_product_data', ['productlink']); + if ( !productdata ) { + return true; + } + + let product_qty = 0; + const product_qty_input = document.querySelectorAll( 'input[name=quantity\\[' + productdata.internal_id + '\\]]' ); + if ( product_qty_input.length > 0 ) { + product_qty = (product_qty_input[0] && product_qty_input[0].value) || 1; + } else { + return true; + } + + if ( 0 == product_qty ) { + return true; + } + productdata.quantity = product_qty; + + delete productdata.internal_id; + + products.push( productdata ); + sum_value += productdata.price * productdata.quantity; + }); + + if ( 0 == products.length ) { + return true; + } + + gtm4wp_push_ecommerce( 'add_to_cart', products, { + 'currency': gtm4wp_currency, + 'value': sum_value.toFixed(2) + }); + + return true; + } + + const product_data_el = product_form.querySelector( '[name=gtm4wp_product_data]' ); + if ( !product_data_el ) { + return true; + } + + let productdata = gtm4wp_read_from_json( product_data_el.value ); + productdata.quantity = product_form.querySelector( '[name=quantity]' ) && product_form.querySelector( '[name=quantity]' ).value; + if ( isNaN( productdata.quantity ) ) { + productdata.quantity = 1; + } + + gtm4wp_push_ecommerce( 'add_to_cart', [ productdata ], { + 'currency': gtm4wp_currency, + 'value': productdata.price * productdata.quantity + }); + + return true; +} function gtm4wp_woocommerce_handle_cart_qty_change() { document.querySelectorAll( '.product-quantity input.qty' ).forEach(function( qty_el ) { @@ -211,119 +447,6 @@ function gtm4wp_woocommerce_process_pages() { return true; } - // track add to cart events for simple products in product lists - if ( event_target_element.closest( '.add_to_cart_button:not(.product_type_variable, .product_type_grouped, .single_add_to_cart_button)' ) ) { - const product_el = event_target_element.closest( '.product,.wc-block-grid__product' ); - - const productdata_el = product_el && product_el.querySelector( '.gtm4wp_productdata' ); - if ( !productdata_el ) { - return true; - } - - const productdata = gtm4wp_read_json_from_node( productdata_el, "gtm4wp_product_data" ); - if ( !productdata ) { - return true; - } - - if ( "variable" === productdata.product_type || "grouped" === productdata.product_type ) { - return true; - } - - if ( productdata.productlink ) { - delete productdata.productlink; - } - delete productdata.product_type; - productdata.quantity = 1; - - gtm4wp_push_ecommerce( 'add_to_cart', [ productdata ], { - 'currency': gtm4wp_currency, - 'value': productdata.price - }); - } - - // track add to cart events for products on product detail pages - const add_to_cart_button = event_target_element.closest( '.single_add_to_cart_button' ); - if ( add_to_cart_button ) { - if (add_to_cart_button.classList.contains( 'disabled' ) || add_to_cart_button.disabled) { - // do not track clicks on disabled buttons - return true; - } - - const product_form = event_target_element.closest( 'form.cart' ); - if ( !product_form ) { - return true; - } - - let product_variant_id = product_form.querySelectorAll( '[name=variation_id]' ); - let product_is_grouped = product_form.classList && product_form.classList.contains( 'grouped_form' ); - - if ( product_variant_id.length > 0 ) { - if ( gtm4wp_last_selected_product_variation ) { - const qty_el = product_form.querySelector( '[name=quantity]' ); - gtm4wp_last_selected_product_variation.quantity = (qty_el && qty_el.value) || 1; - - gtm4wp_push_ecommerce( 'add_to_cart', [ gtm4wp_last_selected_product_variation ], { - 'currency': gtm4wp_currency, - 'value': (gtm4wp_last_selected_product_variation.price * gtm4wp_last_selected_product_variation.quantity).toFixed(2) - }); - } - } else if ( product_is_grouped ) { - const products_in_group = document.querySelectorAll( '.grouped_form .gtm4wp_productdata' ); - let products = []; - let sum_value = 0; - - products_in_group.forEach( function( product_data_el ) { - const productdata = gtm4wp_read_json_from_node(product_data_el, 'gtm4wp_product_data', ['productlink']); - if ( !productdata ) { - return true; - } - - let product_qty = 0; - const product_qty_input = document.querySelectorAll( 'input[name=quantity\\[' + productdata.internal_id + '\\]]' ); - if ( product_qty_input.length > 0 ) { - product_qty = (product_qty_input[0] && product_qty_input[0].value) || 1; - } else { - return true; - } - - if ( 0 == product_qty ) { - return true; - } - productdata.quantity = product_qty; - - delete productdata.internal_id; - - products.push( productdata ); - sum_value += productdata.price * productdata.quantity; - }); - - if ( 0 == products.length ) { - return true; - } - - gtm4wp_push_ecommerce( 'add_to_cart', products, { - 'currency': gtm4wp_currency, - 'value': sum_value.toFixed(2) - }); - } else { - const product_data_el = product_form.querySelector( '[name=gtm4wp_product_data]' ); - if ( !product_data_el ) { - return true; - } - - let productdata = gtm4wp_read_from_json( product_data_el.value ); - productdata.quantity = product_form.querySelector( '[name=quantity]' ) && product_form.querySelector( '[name=quantity]' ).value; - if ( isNaN( productdata.quantity ) ) { - productdata.quantity = 1; - } - - gtm4wp_push_ecommerce( 'add_to_cart', [ productdata ], { - 'currency': gtm4wp_currency, - 'value': productdata.price * productdata.quantity - }); - } - } - // track remove links in mini cart widget and on cart page if ( event_target_element.closest( '.mini_cart_item a.remove,.product-remove a.remove' ) ) { const click_el = event_target_element; @@ -491,6 +614,10 @@ function gtm4wp_woocommerce_process_pages() { } }, { capture: true } ); + if ( !gtm4wp_blocks_integration_enabled ) { + document.addEventListener( 'click', gtm4wp_classic_add_to_cart_click_handler, { capture: true } ); + } + // track variable products on their detail pages // currently, we need to use jQuery here since WooCommerce is firing this event using jQuery // that can not be catched using vanilla JS @@ -691,3 +818,818 @@ if ( document.readyState !== "loading" ) { document.addEventListener( "DOMContentLoaded", gtm4wp_woocommerce_page_loading_completed ); window.addEventListener( "load", gtm4wp_woocommerce_page_loading_completed ); } + +// WooCommerce Blocks add-to-cart tracking +// Only enable if both the option is enabled AND ecommerce tracking is enabled +if ( typeof gtm4wp_blocks_add_to_cart !== 'undefined' && gtm4wp_blocks_add_to_cart ) { + if ( ! window.wc ) { + gtm4wp_blocks_warn_once( 'WooCommerce Blocks scripts are not detected; block add_to_cart events might be unavailable.' ); + } + // Store previous cart state to detect newly added/removed items + let gtm4wp_previous_cart_items = {}; + const gtm4wp_previous_cart_snapshots = {}; + const gtm4wp_product_api_cache = {}; + let gtm4wp_last_cart_signature = null; + let gtm4wp_snapshot_in_progress = false; + let gtm4wp_dom_event_in_progress = false; + const gtm4wp_recent_deltas = {}; + let gtm4wp_pending_cart_snapshot = null; + let gtm4wp_blocks_initialization = null; + let gtm4wp_blocks_initial_state_ready = false; + const gtm4wp_cart_fetch_ttl = 300; + let gtm4wp_cart_fetch_inflight = null; + let gtm4wp_cart_fetch_cache = null; + let gtm4wp_cart_fetch_cache_timestamp = 0; + function gtm4wp_bootstrap_blocks_state_complete( force_ready ) { + if ( force_ready || ! gtm4wp_blocks_initial_state_ready ) { + gtm4wp_blocks_initial_state_ready = true; + } + } + + function gtm4wp_restore_cart_state_from_storage() { + if ( ! gtm4wp_is_session_storage_available() ) { + return false; + } + + try { + const stored_value = window.sessionStorage.getItem( gtm4wp_blocks_cart_storage_key ); + + if ( ! stored_value ) { + return false; + } + + const parsed = JSON.parse( stored_value ); + + if ( ! parsed || parsed.version !== gtm4wp_blocks_cart_storage_version || ! Array.isArray( parsed.items ) ) { + return false; + } + + gtm4wp_previous_cart_items = {}; + Object.keys( gtm4wp_previous_cart_snapshots ).forEach( ( snapshot_key ) => { + delete gtm4wp_previous_cart_snapshots[ snapshot_key ]; + } ); + + parsed.items.forEach( ( entry ) => { + if ( ! entry || ! entry.key ) { + return; + } + + const item_key = entry.key; + const quantity = parseInt( entry.quantity || ( entry.product && entry.product.quantity ) || 0 ); + + if ( isNaN( quantity ) || quantity < 0 ) { + return; + } + + gtm4wp_previous_cart_items[ item_key ] = quantity; + + if ( entry.product ) { + const snapshot = Object.assign( {}, entry.product ); + snapshot.quantity = quantity; + gtm4wp_previous_cart_snapshots[ item_key ] = snapshot; + } + } ); + + gtm4wp_last_cart_signature = parsed.signature || null; + + gtm4wp_bootstrap_blocks_state_complete( true ); + return true; + } catch ( e ) { + return false; + } + } + + function gtm4wp_persist_cart_state_to_storage() { + if ( ! gtm4wp_is_session_storage_available() ) { + return; + } + + try { + const payload = { + version: gtm4wp_blocks_cart_storage_version, + signature: gtm4wp_last_cart_signature, + items: Object.keys( gtm4wp_previous_cart_items ).map( ( item_key ) => { + const quantity = gtm4wp_previous_cart_items[ item_key ]; + const snapshot = gtm4wp_previous_cart_snapshots[ item_key ] || null; + + return { + key: item_key, + quantity: quantity, + product: snapshot, + }; + } ), + timestamp: Date.now(), + }; + + if ( payload.items.length > 0 || payload.signature ) { + window.sessionStorage.setItem( gtm4wp_blocks_cart_storage_key, JSON.stringify( payload ) ); + } else { + window.sessionStorage.removeItem( gtm4wp_blocks_cart_storage_key ); + } + } catch ( e ) { + // Suppress storage errors (private mode, quota, etc.) + } + } + + async function gtm4wp_process_pending_snapshot_if_any() { + if ( ! gtm4wp_pending_cart_snapshot ) { + return; + } + + const pending_snapshot = gtm4wp_pending_cart_snapshot; + gtm4wp_pending_cart_snapshot = null; + await gtm4wp_process_cart_snapshot( pending_snapshot ); + } + +async function gtm4wp_mark_initial_state_ready() { + gtm4wp_bootstrap_blocks_state_complete( true ); + await gtm4wp_process_pending_snapshot_if_any(); + } + + // Function to fetch cart data from WooCommerce Store API + function gtm4wp_fetch_cart_items() { + const now = Date.now(); + + if ( gtm4wp_cart_fetch_cache && ( now - gtm4wp_cart_fetch_cache_timestamp ) < gtm4wp_cart_fetch_ttl ) { + return Promise.resolve( gtm4wp_cart_fetch_cache ); + } + + if ( gtm4wp_cart_fetch_inflight ) { + return gtm4wp_cart_fetch_inflight; + } + + // Try to get cart data from the Store API + const apiUrl = gtm4wp_build_rest_url( 'wc/store/v1/cart' ); + const headers = {}; + + if ( gtm4wp_rest_nonce_value ) { + headers['X-WP-Nonce'] = gtm4wp_rest_nonce_value; + } + + gtm4wp_cart_fetch_inflight = fetch( apiUrl, { + method: 'GET', + headers, + credentials: 'same-origin' + } ) + .then( response => { + if ( !response.ok ) { + // If Store API fails, try to get from dataLayer if available + if ( window.wc && window.wc.store && window.wc.store.cart ) { + return window.wc.store.cart.getCartData(); + } + throw new Error( 'Failed to fetch cart data' ); + } + return response.json(); + } ) + .catch( error => { + console && console.error && console.error( 'GTM4WP: Error fetching cart:', error ); + // Fallback: try to get from WooCommerce store if available + if ( window.wc && window.wc.store && window.wc.store.cart ) { + return window.wc.store.cart.getCartData(); + } + return null; + } ) + .then( ( data ) => { + gtm4wp_cart_fetch_cache = data; + gtm4wp_cart_fetch_cache_timestamp = Date.now(); + return data; + } ) + .finally( () => { + gtm4wp_cart_fetch_inflight = null; + } ); + + return gtm4wp_cart_fetch_inflight; + } + + function gtm4wp_get_cart_item_key( cart_item ) { + const primary_product_id = + cart_item.product_id || + ( cart_item.product && cart_item.product.id ) || + ( cart_item.product && cart_item.product.product_id ) || + cart_item.id || + ''; + + const variation_id = + cart_item.variation_id || + ( cart_item.variation && cart_item.variation.id ) || + ( cart_item.variation && cart_item.variation.variation_id ) || + ''; + + const attribute_source = + ( cart_item.variation && cart_item.variation.attributes ) || + cart_item.attributes || + ( cart_item.variation && cart_item.variation.attributes_data ) || + null; + + const customization_source = cart_item.item_data || cart_item.custom_data || null; + + const attributes_signature = gtm4wp_normalize_key_value_pairs( attribute_source ); + const customization_signature = gtm4wp_normalize_key_value_pairs( customization_source ); + + let signature = primary_product_id + ':' + variation_id + ':' + attributes_signature + ':' + customization_signature; + + if ( 'function' === typeof window.gtm4wp_cart_item_signature_override ) { + try { + const override = window.gtm4wp_cart_item_signature_override( signature, cart_item ); + if ( override ) { + signature = override; + } + } catch ( e ) { + // Ignore override errors + } + } + + return signature; + } + + function gtm4wp_should_register_delta( item_key, delta_qty, delta_type ) { + const absolute_delta = Math.abs( delta_qty ); + + if ( absolute_delta <= 0 ) { + return false; + } + + const type = delta_type || ( delta_qty > 0 ? 'add' : 'remove' ); + const dedupe_key = type + ':' + item_key + ':' + absolute_delta; + const now = Date.now(); + const last_ts = gtm4wp_recent_deltas[ dedupe_key ] || 0; + + if ( now - last_ts < gtm4wp_blocks_dedupe_window_value ) { + return false; + } + + gtm4wp_recent_deltas[ dedupe_key ] = now; + return true; + } + + function gtm4wp_store_cart_item_snapshot( item_key, product_data, quantity ) { + if ( ! item_key || ! product_data ) { + return; + } + + const snapshot = Object.assign( {}, product_data ); + snapshot.quantity = quantity; + gtm4wp_previous_cart_snapshots[ item_key ] = snapshot; + } + + function gtm4wp_bootstrap_blocks_state() { + if ( ! gtm4wp_blocks_initialization ) { + gtm4wp_restore_cart_state_from_storage(); + + gtm4wp_blocks_initialization = gtm4wp_init_blocks_cart_tracking() + .catch( () => {} ) + .finally( () => { + gtm4wp_mark_initial_state_ready(); + } ); + } + + return gtm4wp_blocks_initialization; + } + + async function gtm4wp_ensure_blocks_initialized() { + if ( ! gtm4wp_blocks_initialization ) { + return; + } + + try { + await gtm4wp_blocks_initialization; + } catch ( e ) { + // Ignore initialization errors here; fallbacks will handle missing data. + } + } + + async function gtm4wp_fetch_product_details_via_ajax( product_id ) { + if ( ! gtm4wp_blocks_ajax_endpoint || ! gtm4wp_blocks_product_nonce_value ) { + return null; + } + + const formData = new FormData(); + formData.append( 'action', 'gtm4wp_product_data' ); + formData.append( 'product_id', product_id ); + formData.append( 'nonce', gtm4wp_blocks_product_nonce_value ); + + try { + const response = await fetch( gtm4wp_blocks_ajax_endpoint, { + method: 'POST', + body: formData, + credentials: 'same-origin', + } ); + + if ( ! response.ok ) { + return null; + } + + const data = await response.json(); + const product_payload = ( data && data.success && data.data && data.data.product ) ? data.data.product : null; + + if ( product_payload ) { + gtm4wp_product_api_cache[ product_id ] = product_payload; + } + + return product_payload; + } catch ( ajaxError ) { + return null; + } + } + + async function gtm4wp_fetch_product_details_from_api( product_id ) { + if ( ! product_id ) { + return null; + } + + if ( gtm4wp_product_api_cache[ product_id ] ) { + return gtm4wp_product_api_cache[ product_id ]; + } + + try { + const response = await fetch( gtm4wp_build_rest_url( `gtm4wp/v1/product/${product_id}` ), { + method: 'GET', + credentials: 'same-origin', + headers: gtm4wp_rest_nonce_value ? { 'X-WP-Nonce': gtm4wp_rest_nonce_value } : undefined, + } ); + + if ( response.ok ) { + const data = await response.json(); + + if ( data && data.product ) { + gtm4wp_product_api_cache[ product_id ] = data.product; + return data.product; + } + } else if ( response.status !== 404 ) { + return await gtm4wp_fetch_product_details_via_ajax( product_id ); + } + } catch ( error ) { + console && console.error && console.error( 'GTM4WP: Error fetching product detail', error ); + return await gtm4wp_fetch_product_details_via_ajax( product_id ); + } + + return null; + } + + async function gtm4wp_resolve_parent_identifier( parent_raw_id ) { + if ( ! parent_raw_id ) { + return null; + } + + if ( gtm4wp_use_sku_instead ) { + const parent_details = await gtm4wp_fetch_product_details_from_api( parent_raw_id ); + + if ( parent_details && parent_details.item_id ) { + return parent_details.item_id; + } + } + + return parent_raw_id; + } + + // Function to convert WooCommerce cart item to GA4 product format + async function gtm4wp_convert_cart_item_to_product( cart_item ) { + // Handle different cart item structures from Store API + const product = cart_item.variation || cart_item.product || cart_item; + + // Get product ID - use variation ID if available, otherwise product ID + let product_id = product.id || cart_item.id; + let item_id = product_id; + + // Check if this is a variation + if ( cart_item.variation && cart_item.variation.id ) { + product_id = cart_item.variation.id; + item_id = cart_item.variation.id; + } else if ( cart_item.variation_id ) { + product_id = cart_item.variation_id; + item_id = cart_item.variation_id; + } + + // Use SKU if configured + const sku = product.sku || cart_item.sku || ''; + if ( gtm4wp_use_sku_instead && sku && ( '' !== sku ) ) { + item_id = sku; + } + + // Get price - handle different price formats (cents vs dollars) + let price = 0; + if ( product.prices && product.prices.price ) { + price = parseFloat( product.prices.price ) / 100; // Convert from cents + } else if ( product.price ) { + price = parseFloat( product.price ); + } else if ( cart_item.prices && cart_item.prices.price ) { + price = parseFloat( cart_item.prices.price ) / 100; + } else if ( cart_item.price ) { + price = parseFloat( cart_item.price ); + } + + // Build product data object matching the format used by gtm4wp_woocommerce_process_product + const product_type = product.type || cart_item.type || ''; + const internal_id = product.id || cart_item.id || product_id; + + const product_data = { + item_id: item_id, + item_name: product.name || cart_item.name || '', + price: gtm4wp_make_sure_is_float( price ), + quantity: cart_item.quantity || 1, + item_type: product_type || 'simple', + }; + + // Add SKU (fallback to product_id if no SKU) + product_data.sku = sku || product_id; + + // Add item_group_id for variations (parent product ID) + if ( ( cart_item.variation && cart_item.variation.id ) || cart_item.variation_id ) { + const parent_raw_id = cart_item.id || product.id || product_id; + const resolved_parent_id = await gtm4wp_resolve_parent_identifier( parent_raw_id ); + + if ( resolved_parent_id && resolved_parent_id !== item_id ) { + product_data.item_group_id = resolved_parent_id; + } + } + + // Add grouped parent ID if available. + if ( ! product_data.item_group_id ) { + const grouped_parent_raw_id = + ( cart_item.group_id ) + || ( cart_item.parent && cart_item.parent.id ) + || cart_item.parent_id + || product.parent_id + || ( product.parent && product.parent.id ) + || ( cart_item.extensions && cart_item.extensions.grouped && cart_item.extensions.grouped.parent_id ); + + const resolved_group_parent_id = await gtm4wp_resolve_parent_identifier( grouped_parent_raw_id ); + + if ( resolved_group_parent_id && resolved_group_parent_id !== item_id ) { + product_data.item_group_id = resolved_group_parent_id; + } + } + + const api_product_details = await gtm4wp_fetch_product_details_from_api( internal_id ); + if ( api_product_details ) { + const mergeable_fields = [ + 'item_category', + 'item_category2', + 'item_category3', + 'item_category4', + 'item_category5', + 'item_brand', + 'google_business_vertical', + 'stockstatus', + 'stocklevel', + 'item_group_id', + 'item_type', + 'item_variant', + 'item_group_sku', + ]; + + mergeable_fields.forEach( ( field ) => { + if ( ! product_data[field] && api_product_details[field] ) { + product_data[field] = api_product_details[field]; + } + } ); + } + + // Add categories if available + const categories = product.categories || cart_item.categories || []; + if ( categories.length > 0 ) { + product_data.item_category = categories[0].name || categories[0] || ''; + if ( categories.length > 1 ) { + product_data.item_category2 = categories[1].name || categories[1] || ''; + } + if ( categories.length > 2 ) { + product_data.item_category3 = categories[2].name || categories[2] || ''; + } + if ( categories.length > 3 ) { + product_data.item_category4 = categories[3].name || categories[3] || ''; + } + if ( categories.length > 4 ) { + product_data.item_category5 = categories[4].name || categories[4] || ''; + } + } + + // Add brand if available + if ( product.brand || cart_item.brand ) { + product_data.item_brand = product.brand || cart_item.brand; + } + + // Add variant attributes if available + if ( cart_item.variation && cart_item.variation.attributes ) { + const variant_attrs = []; + for ( const key in cart_item.variation.attributes ) { + if ( cart_item.variation.attributes.hasOwnProperty( key ) ) { + variant_attrs.push( cart_item.variation.attributes[key] ); + } + } + if ( variant_attrs.length > 0 ) { + product_data.item_variant = variant_attrs.join( ',' ); + } + } else if ( cart_item.variation && cart_item.variation.attributes ) { + // Alternative structure + const variant_attrs = Object.values( cart_item.variation.attributes ); + if ( variant_attrs.length > 0 ) { + product_data.item_variant = variant_attrs.join( ',' ); + } + } + + return product_data; + } + + async function gtm4wp_process_cart_snapshot( cart_data ) { + if ( ! cart_data ) { + return false; + } + + if ( ! gtm4wp_blocks_initial_state_ready ) { + gtm4wp_pending_cart_snapshot = cart_data; + gtm4wp_bootstrap_blocks_state(); + return false; + } + + const cart_items = cart_data.items || cart_data.cartItems || []; + const cart_items_map = {}; + const cart_signature_components = []; + + cart_items.forEach( ( cart_item ) => { + const item_key = gtm4wp_get_cart_item_key( cart_item ); + + if ( ! item_key ) { + return; + } + + let quantity_raw = cart_item.quantity; + if ( 'object' === typeof quantity_raw && null !== quantity_raw ) { + quantity_raw = quantity_raw.value || quantity_raw.count || quantity_raw.qty || 0; + } + + const quantity = parseInt( quantity_raw || cart_item.quantity_total || cart_item.qty || 0 ); + + cart_items_map[ item_key ] = { + cart_item, + quantity, + }; + + cart_signature_components.push( item_key + ':' + quantity ); + } ); + + const cart_signature = cart_signature_components.sort().join( '|' ); + + if ( cart_signature === gtm4wp_last_cart_signature ) { + return false; + } + + const new_items = []; + const removed_items = []; + let total_value = 0; + let removed_value = 0; + const processed_keys = {}; + + const processPromises = Object.keys( cart_items_map ).map( async ( item_key ) => { + const entry = cart_items_map[ item_key ]; + const current_qty = entry.quantity; + const previous_qty = gtm4wp_previous_cart_items[ item_key ] || 0; + + if ( current_qty === previous_qty ) { + processed_keys[ item_key ] = true; + return; + } + + const product_data = await gtm4wp_convert_cart_item_to_product( entry.cart_item ); + + if ( product_data ) { + gtm4wp_store_cart_item_snapshot( item_key, product_data, current_qty ); + + if ( current_qty > previous_qty ) { + const delta = current_qty - previous_qty; + const addition_payload = Object.assign( {}, product_data, { quantity: delta } ); + + if ( gtm4wp_should_register_delta( item_key, delta, 'add' ) ) { + new_items.push( addition_payload ); + total_value += parseFloat( addition_payload.price || 0 ) * addition_payload.quantity; + } + } else if ( previous_qty > current_qty ) { + const delta = previous_qty - current_qty; + const removal_payload = Object.assign( {}, product_data, { quantity: delta } ); + + if ( gtm4wp_should_register_delta( item_key, delta, 'remove' ) ) { + removed_items.push( removal_payload ); + removed_value += parseFloat( removal_payload.price || 0 ) * removal_payload.quantity; + } + } + } + + gtm4wp_previous_cart_items[ item_key ] = current_qty; + processed_keys[ item_key ] = true; + } ); + + await Promise.all( processPromises ); + + const previous_keys = Object.keys( gtm4wp_previous_cart_items ); + + previous_keys.forEach( ( item_key ) => { + if ( processed_keys[ item_key ] || cart_items_map[ item_key ] ) { + return; + } + + const previous_qty = gtm4wp_previous_cart_items[ item_key ] || 0; + + if ( previous_qty <= 0 ) { + delete gtm4wp_previous_cart_items[ item_key ]; + delete gtm4wp_previous_cart_snapshots[ item_key ]; + return; + } + + const snapshot = gtm4wp_previous_cart_snapshots[ item_key ]; + + if ( snapshot ) { + const removal_payload = Object.assign( {}, snapshot, { quantity: previous_qty } ); + + if ( gtm4wp_should_register_delta( item_key, previous_qty, 'remove' ) ) { + removed_items.push( removal_payload ); + removed_value += parseFloat( removal_payload.price || 0 ) * removal_payload.quantity; + } + } + + delete gtm4wp_previous_cart_items[ item_key ]; + delete gtm4wp_previous_cart_snapshots[ item_key ]; + } ); + + let changes_detected = false; + + if ( new_items.length > 0 ) { + gtm4wp_push_ecommerce( 'add_to_cart', new_items, { + 'currency': gtm4wp_currency, + 'value': total_value.toFixed(2) + } ); + changes_detected = true; + } + + if ( removed_items.length > 0 ) { + gtm4wp_push_ecommerce( 'remove_from_cart', removed_items, { + 'currency': gtm4wp_currency, + 'value': removed_value.toFixed(2) + } ); + changes_detected = true; + } + + gtm4wp_last_cart_signature = cart_signature; + gtm4wp_persist_cart_state_to_storage(); + return changes_detected; + } + + // Function to process add-to-cart event + async function gtm4wp_process_blocks_add_to_cart( event ) { + if ( gtm4wp_dom_event_in_progress ) { + return; + } + + gtm4wp_bootstrap_blocks_state(); + await gtm4wp_ensure_blocks_initialized(); + + gtm4wp_dom_event_in_progress = true; + + if ( gtm4wp_snapshot_in_progress ) { + gtm4wp_dom_event_in_progress = false; + return; + } + + gtm4wp_snapshot_in_progress = true; + + try { + const retrySchedule = [ 0, 250, 600 ]; + let processedSnapshot = false; + + for ( let retryIndex = 0; retryIndex < retrySchedule.length; retryIndex++ ) { + const delay = retrySchedule[ retryIndex ]; + + if ( delay > 0 ) { + await gtm4wp_delay( delay ); + } + + const cart_data = await gtm4wp_fetch_cart_items(); + processedSnapshot = await gtm4wp_process_cart_snapshot( cart_data ); + + if ( processedSnapshot ) { + break; + } + } + } finally { + gtm4wp_snapshot_in_progress = false; + gtm4wp_dom_event_in_progress = false; + } + } + + // Initialize: Fetch initial cart state when page loads + async function gtm4wp_init_blocks_cart_tracking() { + const cart_data = await gtm4wp_fetch_cart_items(); + + if ( ! cart_data ) { + await gtm4wp_mark_initial_state_ready(); + return; + } + + gtm4wp_previous_cart_items = {}; + Object.keys( gtm4wp_previous_cart_snapshots ).forEach( ( snapshot_key ) => { + delete gtm4wp_previous_cart_snapshots[ snapshot_key ]; + } ); + + const cart_items = cart_data.items || cart_data.cartItems || []; + + await Promise.all( cart_items.map( async ( cart_item ) => { + const item_key = gtm4wp_get_cart_item_key( cart_item ); + + if ( ! item_key ) { + return; + } + + const quantity = parseInt( cart_item.quantity || 0 ); + gtm4wp_previous_cart_items[item_key] = quantity; + + const product_data = await gtm4wp_convert_cart_item_to_product( cart_item ); + + if ( product_data ) { + gtm4wp_store_cart_item_snapshot( item_key, product_data, quantity ); + } + } ) ); + + gtm4wp_last_cart_signature = cart_items + .map( ( cart_item ) => gtm4wp_get_cart_item_key( cart_item ) + ':' + parseInt( cart_item.quantity || 0 ) ) + .sort() + .join( '|' ); + + await gtm4wp_mark_initial_state_ready(); + gtm4wp_persist_cart_state_to_storage(); + } + + function gtm4wp_register_blocks_add_to_cart_listener( target ) { + if ( !target || target._gtm4wp_wc_blocks_listener_registered ) { + return; + } + + target.addEventListener( 'wc-blocks_added_to_cart', function( event ) { + if ( event._gtm4wp_handled ) { + return; + } + + event._gtm4wp_handled = true; + + // Small delay to ensure cart is updated on the server + setTimeout( function() { + gtm4wp_process_blocks_add_to_cart( event ); + }, 200 ); + }, true ); + + target._gtm4wp_wc_blocks_listener_registered = true; + } + + function gtm4wp_subscribe_to_cart_changes() { + if ( ! gtm4wp_is_blocks_store_available() ) { + gtm4wp_blocks_warn_once( 'WooCommerce Blocks data store unavailable; mini-cart add_to_cart tracking is disabled.' ); + return; + } + + const cartStoreHasData = () => { + const cartStore = wp.data.select( 'wc/store/cart' ); + return cartStore && cartStore.getCartData ? cartStore.getCartData() : null; + }; + + wp.data.subscribe( async () => { + const cartData = cartStoreHasData(); + if ( ! cartData ) { + return; + } + + await gtm4wp_ensure_blocks_initialized(); + + if ( ! gtm4wp_blocks_initial_state_ready ) { + return; + } + + const cart_items = cartData.items || cartData.cartItems || []; + const current_signature = cart_items + .map( ( cart_item ) => gtm4wp_get_cart_item_key( cart_item ) + ':' + parseInt( cart_item.quantity || 0 ) ) + .sort() + .join( '|' ); + + if ( current_signature === gtm4wp_last_cart_signature || gtm4wp_snapshot_in_progress ) { + return; + } + + if ( gtm4wp_blocks_initial_state_ready && ! gtm4wp_dom_event_in_progress ) { + await gtm4wp_process_cart_snapshot( cartData ); + } else { + gtm4wp_bootstrap_blocks_state_complete( true ); + gtm4wp_last_cart_signature = current_signature; + } + } ); + } + + function gtm4wp_start_blocks_tracking() { + gtm4wp_bootstrap_blocks_state(); + gtm4wp_register_blocks_add_to_cart_listener( document ); + gtm4wp_register_blocks_add_to_cart_listener( document.body ); + gtm4wp_subscribe_to_cart_changes(); + } + + // Listen for WooCommerce Blocks add-to-cart DOM event + if ( document.readyState === 'loading' ) { + document.addEventListener( 'DOMContentLoaded', gtm4wp_start_blocks_tracking ); + } else { + gtm4wp_start_blocks_tracking(); + } +} diff --git a/readme.txt b/readme.txt index 823d5a2..983d036 100644 --- a/readme.txt +++ b/readme.txt @@ -5,7 +5,7 @@ Tags: google tag manager, tag manager, gtm, google ads, google analytics Requires at least: 3.4.0 Requires PHP: 7.4 Tested up to: 6.8 -Stable tag: 1.22.2 +Stable tag: 1.22.3 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl.html @@ -224,6 +224,10 @@ to report micro conversions and/or to serve ads only to visitors who spend more == Changelog == += 1.22.3 = + +* Added: new WooCommerce Add to Cart block trackign for adds and removes from cart + = 1.22.2 = * Fixed: purchase event was not fired when is_order_received_page() WooCommerce tag was not supported by the template and the fallback method had to activate. @@ -352,6 +356,10 @@ If you are on GA360 and still collecting ecommerce data, you need to update your == Upgrade Notice == += 1.22.3 = + +Minor release with improved WooCommerce Blocks health checks and admin option sanitizing. + = 1.22.2 = Bugfix release