diff --git a/classes/Database/Install.php b/classes/Database/Install.php index 7233e725..a74b1493 100644 --- a/classes/Database/Install.php +++ b/classes/Database/Install.php @@ -106,6 +106,7 @@ public function registerHooks() $this->module->registerHook('displayAdminOrder') && $this->module->registerHook('displayBeforeBodyClosingTag') && $this->module->registerHook('displayFooterProduct') && + $this->module->registerHook('displayProductAdditionalInfo') && $this->module->registerHook('displayOrderConfirmation') && $this->module->registerHook('actionProductCancel') && $this->module->registerHook('actionValidateOrder') && diff --git a/classes/Handler/GanalyticsJsHandler.php b/classes/Handler/GanalyticsJsHandler.php index c4c87c39..9114c1b9 100644 --- a/classes/Handler/GanalyticsJsHandler.php +++ b/classes/Handler/GanalyticsJsHandler.php @@ -43,6 +43,33 @@ public function __construct(Ps_Googleanalytics $module, Context $context) * @return void|string */ public function generate($jsCode) + { + return $this->render($jsCode, 'ga_tag.tpl'); + } + + /** + * Generate Google Analytics js that runs as soon as it is parsed. + * + * ga_tag.tpl defers to DOMContentLoaded, which never fires again once the page has loaded, so + * anything injected into an already-rendered page - a block swapped in by an AJAX refresh - + * has to run immediately or not at all. + * + * @param string $jsCode + * + * @return void|string + */ + public function generateImmediate($jsCode) + { + return $this->render($jsCode, 'ga_tag_immediate.tpl'); + } + + /** + * @param string $jsCode + * @param string $template + * + * @return void|string + */ + private function render($jsCode, $template) { if (Configuration::get('GA_ACCOUNT_ID')) { $this->context->smarty->assign( @@ -53,7 +80,7 @@ public function generate($jsCode) return $this->module->display( $this->module->getLocalPath() . $this->module->name, - 'ga_tag.tpl' + $template ); } } diff --git a/classes/Hook/HookDisplayProductAdditionalInfo.php b/classes/Hook/HookDisplayProductAdditionalInfo.php new file mode 100644 index 00000000..1936a597 --- /dev/null +++ b/classes/Hook/HookDisplayProductAdditionalInfo.php @@ -0,0 +1,101 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +namespace PrestaShop\Module\Ps_Googleanalytics\Hooks; + +use Context; +use PrestaShop\Module\Ps_Googleanalytics\Handler\GanalyticsJsHandler; +use PrestaShop\Module\Ps_Googleanalytics\Wrapper\ProductWrapper; +use Ps_Googleanalytics; +use Tools; + +class HookDisplayProductAdditionalInfo implements HookInterface +{ + private $module; + private $context; + + public function __construct(Ps_Googleanalytics $module, Context $context) + { + $this->module = $module; + $this->context = $context; + } + + /** + * run + * + * Re-sends view_item after the shopper picks another combination. + * + * The product page swaps a handful of blocks over AJAX and pushes a new URL instead of + * reloading, so displayFooterProduct - which carries view_item - is never rendered again and + * Google Analytics keeps reporting the combination the page was opened with. This partial IS + * one of the blocks ProductController returns for that refresh, so hooking it here reports the + * combination the shopper is actually looking at. + * + * @return string|void + */ + public function run() + { + /* + * Only the product refresh, nothing else. On a normal page render displayFooterProduct has + * already sent view_item, and emitting here as well would double every product view. + */ + if (!Tools::getValue('ajax') || Tools::getValue('action') !== 'refresh') { + return; + } + + // Quickview has never reported to Google Analytics; its refresh should not either. + if (Tools::getValue('quickview')) { + return; + } + + if ($this->context->controller->php_self !== 'product') { + return; + } + + $product = $this->context->smarty->getTemplateVars('product'); + if (empty($product)) { + return; + } + + /* + * The 'product' template variable is expected to be a ProductLazyArray (array-accessible). + * Some themes or modules assign a raw Product object instead, which prepareItemFromProduct() + * cannot consume. Skip tracking in that case rather than breaking the refresh. + */ + if (!is_array($product) && !$product instanceof \ArrayAccess) { + return; + } + + $productWrapper = new ProductWrapper($this->context); + $item = $productWrapper->prepareItemFromProduct($product); + + $jsCode = $this->module->getTools()->renderEvent( + 'view_item', + [ + 'currency' => $this->context->currency->iso_code, + 'value' => $item['price'], + 'items' => [$item], + ] + ); + + // Injected into a page that has already loaded, so it must not wait for DOMContentLoaded. + return (new GanalyticsJsHandler($this->module, $this->context))->generateImmediate($jsCode); + } +} diff --git a/config.xml b/config.xml index cb9bf001..b564895e 100644 --- a/config.xml +++ b/config.xml @@ -2,7 +2,7 @@ ps_googleanalytics - + diff --git a/ps_googleanalytics.php b/ps_googleanalytics.php index fa3cfee7..7377474d 100644 --- a/ps_googleanalytics.php +++ b/ps_googleanalytics.php @@ -47,7 +47,7 @@ public function __construct() { $this->name = 'ps_googleanalytics'; $this->tab = 'analytics_stats'; - $this->version = '6.0.0'; + $this->version = '6.1.0'; $this->ps_versions_compliancy = ['min' => '8.2.0', 'max' => _PS_VERSION_]; $this->author = 'PrestaShop'; $this->module_key = 'fd2aaefea84ac1bb512e6f1878d990b8'; @@ -120,6 +120,18 @@ public function hookDisplayFooterProduct() return $hook->run(); } + /** + * Product page additional-info hook + * This block is re-rendered when the shopper picks another combination, which is where + * view_item has to be sent again + */ + public function hookDisplayProductAdditionalInfo() + { + $hook = new PrestaShop\Module\Ps_Googleanalytics\Hooks\HookDisplayProductAdditionalInfo($this, $this->context); + + return $hook->run(); + } + /** * Hook admin order. * This function is run to send transactions and refunds details diff --git a/upgrade/upgrade-6.1.0.php b/upgrade/upgrade-6.1.0.php new file mode 100644 index 00000000..e7851096 --- /dev/null +++ b/upgrade/upgrade-6.1.0.php @@ -0,0 +1,32 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + * International Registered Trademark & Property of PrestaShop SA + */ +if (!defined('_PS_VERSION_')) { + exit; +} + +/** + * @param Ps_Googleanalytics $object + * + * @return bool + */ +function upgrade_module_6_1_0($object) +{ + return $object->registerHook('displayProductAdditionalInfo'); +} diff --git a/views/templates/hook/ga_tag_immediate.tpl b/views/templates/hook/ga_tag_immediate.tpl new file mode 100644 index 00000000..ed924bbf --- /dev/null +++ b/views/templates/hook/ga_tag_immediate.tpl @@ -0,0 +1,26 @@ +{** + * Copyright since 2007 PrestaShop SA and Contributors + * + * NOTICE OF LICENSE + * + * This source file is subject to the Academic Free License 3.0 (AFL-3.0) + * that is bundled with this package in the file LICENSE.md. + * It is also available through the world-wide-web at this URL: + * https://opensource.org/licenses/AFL-3.0 + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@prestashop.com so we can send you a copy immediately. + * + * @author PrestaShop SA + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + * International Registered Trademark & Property of PrestaShop SA + *} + +{* Same as ga_tag.tpl, without the DOMContentLoaded wrapper: this block is injected into a page + that has already loaded, so waiting for that event would mean the code never runs. *} +{if (!empty($jsCode))} + +{/if}