From 3b95f1c0878760158f15d459485e7a3e533d0876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20R=C4=85czka?= Date: Sat, 9 Nov 2024 11:54:30 +0100 Subject: [PATCH 01/14] Settings grid --- src/Service/SettingsService.php | 122 +++++++++++++++++++++++++++++++- src/Util/WpSettingsUtil.php | 37 ++++++++-- 2 files changed, 152 insertions(+), 7 deletions(-) diff --git a/src/Service/SettingsService.php b/src/Service/SettingsService.php index ab86f51..cf6e719 100644 --- a/src/Service/SettingsService.php +++ b/src/Service/SettingsService.php @@ -199,7 +199,48 @@ public function settingsInit() { 'event_inspector', 'Event Inspector', 'Events Inspector provide basic way of confirming that events are being tracked. Depending on the setting below it will show a small window at the bottom of every page with all eCommerce events captured during a given session.', - 'tools' + 'tools', + [ 'grid' => 'start' ] + ); + + $this->wpSettingsUtil->addSettingsSection( + 'event_deferral', + 'Event Deferral', + 'Defer events pushed to data layer until "DOM ready" event. Useful when using asynchronous Consent Management Platform to ensure consent state is provided before firing any events.', + 'tools', + [ 'grid' => 'item', 'badge' => 'PRO' ] + ); + + $this->wpSettingsUtil->addSettingsSection( + 'cart_data_gads', + 'Google Ads Cart Data', + 'Pass additional Cart Data parameters to your Google Ads Conversions. This allows to use Dynamic Remarketing campaigns or track conversion value based on COGS information provided in your Google Merchant Center Account.', + 'tools', + [ 'grid' => 'item', 'badge' => 'PRO' ] + ); + + $this->wpSettingsUtil->addSettingsSection( + 'server_side_endpoint_cogs', + 'sGTM COGS Endpoint', + 'When using server-side GTM you can make additional transformation before data is sent to the 3rd party platform. This endpoint allows replacing default revenue based conversion values with profit information stored in WooCommerce using COGS plugin.', + 'tools', + [ 'grid' => 'item', 'badge' => 'PRO' ] + ); + + $this->wpSettingsUtil->addSettingsSection( + 'product_feed_google', + 'Google Product Feed', + '', + 'tools', + [ 'grid' => 'item' ] + ); + + $this->wpSettingsUtil->addSettingsSection( + 'product_feed_facebook', + 'Facebook Product Feed', + '', + 'tools', + [ 'grid' => 'end' ] ); $this->wpSettingsUtil->addSettingsField( @@ -235,6 +276,85 @@ public function settingsInit() { ] ); + $this->wpSettingsUtil->addSettingsField( + 'defer_events', + 'Defer events?', + [$this, 'checkboxField'], + 'event_deferral', + 'When clicked Ecommerce events will be pushed to DataLayer after DOM Ready event.', + ['disabled' => true, 'title' => 'Upgrade to PRO version above.'] + ); + + $this->wpSettingsUtil->addSettingsField( + 'google_business_vertical', + 'Google Business Vertical', + [$this, 'selectField'], + 'cart_data_gads', + 'Select required parameter learn more here.', + [ + 'options' => [ + 'no' => 'Feature Disabled', + 'retail' => 'Retail', + 'education' => 'education' + ], + 'disabled' => true, + 'title' => 'Upgrade to PRO version above.'] + ); + + $this->wpSettingsUtil->addSettingsField( + 'dynamic_remarketing_google_id_prefix', + 'Item ID prefix', + [$this, 'inputField'], + 'cart_data_gads', + 'Optional prefix to match your product feed in Google Merchant Center, e.g. `woocommerce_gpf_`.', + [ + 'type' => 'text', + 'disabled' => true, + 'title' => 'Upgrade to PRO version above.' + ] + ); + + $this->wpSettingsUtil->addSettingsField( + 'cart_data_google_merchant_id', + 'AW Merchant ID', + [$this, 'inputField'], + 'cart_data_gads', + 'The Merchant Center ID. Provide this parameter if you submit an item in several Merchant Center accounts and you want to control from which Merchant Center the item’s data, for example, its COGS, should be read.', + [ + 'type' => 'text', + 'disabled' => true, + 'title' => 'Upgrade to PRO version above.' + ] + ); + + $this->wpSettingsUtil->addSettingsField( + 'server_side_endpoint_cogs_enabled', + 'Enable COGS Endpoint', + [$this, 'checkboxField'], + 'server_side_endpoint_cogs', + 'Enable serving COGS information for sGTM container.', + [ + 'disabled' => true, + 'title' => 'Upgrade to PRO version above.' + ] + ); + + $this->wpSettingsUtil->addSettingsField( + 'product_feed_google_enabled', + 'Enable Product Feed', + [$this, 'checkboxField'], + 'product_feed_google', + 'Generate Product Feed for Google Merchant Center.' + ); + + $this->wpSettingsUtil->addSettingsField( + 'product_feed_facebook_enabled', + 'Enable Product Feed', + [$this, 'checkboxField'], + 'product_feed_facebook', + 'Generate Product Feed for Facebook / Meta Product Catalog.' + ); + $this->wpSettingsUtil->addSettingsField( 'gtm_snippet_prevent_load', 'Prevent loading GTM Snippet?', diff --git a/src/Util/WpSettingsUtil.php b/src/Util/WpSettingsUtil.php index 33c05c1..509fcc6 100644 --- a/src/Util/WpSettingsUtil.php +++ b/src/Util/WpSettingsUtil.php @@ -44,21 +44,46 @@ public function addTab( $tabName, $tabTitle, $showSaveButton = true) { ]; } - public function addSettingsSection( $sectionName, $sectionTitle, $description, $tab): void { + public function addSettingsSection( $sectionName, $sectionTitle, $description, $tab, $extra = null): void { $this->sections[$sectionName] = [ 'name' => $sectionName, 'tab' => $tab ]; + $args = [ + 'before_section' => '', + 'after_section' => '', + ]; + + $grid = isset($extra['grid']) ? $extra['grid'] : null; + $badge = isset($extra['badge']) ? $extra['badge'] : null; + + if ($grid === 'start' || $grid === 'single') { + $args['before_section'] = '
'; + } + if ($grid !== null) { + $args['before_section'] .= '
'; + $args['after_section'] = '
'; + } + + if ($grid === 'end' || $grid === 'single') { + $args['after_section'] .= '

'; + } + + $title = __( $sectionTitle, $this->spineCaseNamespace ); + if ($badge) { + $title .= ' ' . strtoupper($badge) . ''; + } + add_settings_section( $this->snakeCaseNamespace . '_' . $sectionName, - __( $sectionTitle, $this->spineCaseNamespace ), - static function( $args) use ( $description) { + $title, + static function( $args) use ( $description, $grid ) { ?> -

- snakeCaseNamespace . '_' . $tab + $this->snakeCaseNamespace . '_' . $tab, + $args ); } From 06163cd2b4ae005805434b1c1d27bf41ff291a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20R=C4=85czka?= Date: Sun, 10 Nov 2024 20:11:24 +0100 Subject: [PATCH 02/14] Settings gallery, product feed and new inspector --- .editorconfig | 3 + assets/gtm-ecommerce-woo-event-inspector.css | 139 +++++++++++ assets/gtm-ecommerce-woo-event-inspector.js | 51 ++-- gtm-ecommerce-woo.php | 3 +- readme.txt | 4 +- src/Container.php | 11 + src/Service/EventInspectorService.php | 61 +++-- src/Service/ProductFeedService.php | 243 +++++++++++++++++++ src/Service/SettingsService.php | 25 +- src/Util/SanitizationUtil.php | 4 + src/Util/WcOutputUtil.php | 20 ++ 11 files changed, 512 insertions(+), 52 deletions(-) create mode 100644 assets/gtm-ecommerce-woo-event-inspector.css create mode 100644 src/Service/ProductFeedService.php diff --git a/.editorconfig b/.editorconfig index 8d44760..bb698fc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,6 +13,9 @@ indent_style = tab insert_final_newline = true trim_trailing_whitespace = true +[*.{js,css}] +indent_style = space + [*.txt] trim_trailing_whitespace = false diff --git a/assets/gtm-ecommerce-woo-event-inspector.css b/assets/gtm-ecommerce-woo-event-inspector.css new file mode 100644 index 0000000..d2e7fe8 --- /dev/null +++ b/assets/gtm-ecommerce-woo-event-inspector.css @@ -0,0 +1,139 @@ +#gtm-ecommerce-woo-event-inspector { + position: fixed; + bottom: 0; + right: 0; + width: 100%; + max-height: 95%; + height: 600px; + background-color: #ffffff; + font-family: 'Roboto', Arial, sans-serif; + font-size: 14px; + z-index: 9999; + overflow: hidden; + box-shadow: 0 -4px 6px rgba(0, 0, 0, 0.1); +} + +@media (min-width: 768px) { + #gtm-ecommerce-woo-event-inspector { + width: 400px; + bottom: 20px; + right: 20px; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08); + } +} + +#gtm-ecommerce-woo-event-inspector.minimized { + height: auto; +} + +#gtm-ecommerce-woo-event-inspector.minimized .tabs, +#gtm-ecommerce-woo-event-inspector.minimized .content { + display: none; +} + +#gtm-ecommerce-woo-event-inspector .header { + background-color: #4285f4; + color: #ffffff; + padding: 0 15px; + font-size: 16px; + font-weight: 500; + display: flex; + justify-content: space-between; + align-items: center; +} + +#gtm-ecommerce-woo-event-inspector .content { + height: calc(100% - 56px); + overflow-y: auto; +} + +#gtm-ecommerce-woo-event-inspector .tabs { + display: flex; + background-color: #f1f3f4; + border-bottom: 1px solid #dadce0; +} + +#gtm-ecommerce-woo-event-inspector .tab { + padding: 12px 16px; + cursor: pointer; + color: #5f6368; + font-weight: 500; + transition: background-color 0.2s, color 0.2s; +} + +#gtm-ecommerce-woo-event-inspector .tab:hover { + background-color: #e8f0fe; +} + +#gtm-ecommerce-woo-event-inspector .tab.active { + color: #1a73e8; + border-bottom: 2px solid #1a73e8; +} + +#gtm-ecommerce-woo-event-inspector .tab-content { + display: none; + height: calc(100% - 49px); + overflow-y: auto; + padding: 16px; +} + +#gtm-ecommerce-woo-event-inspector .tab-content.active { + display: block; +} + +#gtm-ecommerce-woo-event-inspector .event { + margin-bottom: 16px; + border-bottom: 1px solid #e0e0e0; + padding-bottom: 12px; +} + +#gtm-ecommerce-woo-event-inspector .event-name { + font-weight: 500; + color: #202124; + margin-bottom: 4px; +} + +#gtm-ecommerce-woo-event-inspector .event-properties { + margin-left: 16px; + color: #5f6368; +} + +#gtm-ecommerce-woo-event-inspector .clear-history { + cursor: pointer; + color: #ffffff; + border: 1px solid #fff; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + background: none; +} + +#gtm-ecommerce-woo-event-inspector .toggle-size { + cursor: pointer; + background: none; + border: none; + color: #ffffff; + font-size: 20px; +} + +#gtm-ecommerce-woo-event-inspector pre { + white-space: pre-wrap; + word-wrap: break-word; + background-color: #f8f9fa; + border: 1px solid #dadce0; + border-radius: 4px; + padding: 8px; + color: #202124; +} + +#gtm-ecommerce-woo-event-inspector h3 { + color: #202124; + font-weight: 500; + margin-top: 16px; + margin-bottom: 8px; +} + +#gtm-ecommerce-woo-event-inspector ul { + margin: 0; +} diff --git a/assets/gtm-ecommerce-woo-event-inspector.js b/assets/gtm-ecommerce-woo-event-inspector.js index c0ed1f8..a9b782b 100644 --- a/assets/gtm-ecommerce-woo-event-inspector.js +++ b/assets/gtm-ecommerce-woo-event-inspector.js @@ -1,4 +1,4 @@ -(function($) { +(function($, w) { dataLayer = window.dataLayer || []; var dataLayerIndex = 0; @@ -57,38 +57,41 @@ eventName = "checkout (UA)"; } - return template.replace('{{event}}', eventName); + return template + .replace('{{event}}', eventName) + .replace('{{json}}', JSON.stringify(item, null, 2)); }); $("#gtm-ecommerce-woo-event-inspector-list").html(rendered); } $(function() { - $("#gtm-ecommerce-woo-event-inspector-list").on("click", "li", function(ev) { - var index = $(ev.target).index(); - var existingStoredEvents = JSON.parse(sessionStorage.getItem("gtmDatalayerDebugger")) || []; - // since items are stored in chronological order, but we render them in reverse order: - alert(JSON.stringify(existingStoredEvents.reverse()[index], null, 2)); + // Toggle tool size + $('#gtm-ecommerce-woo-event-inspector').on('click', '.toggle-size', function() { + $('#gtm-ecommerce-woo-event-inspector').toggleClass('minimized'); + sessionStorage.setItem('gtmDatalayerDebuggerMinimized', $('#gtm-ecommerce-woo-event-inspector').hasClass('minimized')); + }); + + if (sessionStorage.getItem('gtmDatalayerDebuggerMinimized') === 'true') { + $('#gtm-ecommerce-woo-event-inspector').addClass('minimized'); + } + + $('#gtm-ecommerce-woo-event-inspector').on('click', '.clear-history', function() { + sessionStorage.setItem("gtmDatalayerDebugger", '[]'); + renderItems([]); + }); + + $("#gtm-ecommerce-woo-event-inspector-list").on("click", "li span", function(ev) { + $(ev.currentTarget).siblings('pre').toggle(); }); var existingStoredEvents = JSON.parse(sessionStorage.getItem("gtmDatalayerDebugger")) || []; renderItems(existingStoredEvents); setInterval(checkDataLayer, 100); - }); + if (w.hljs) { + w.hljs.highlightAll(); + } - // dataLayer.push = function() { - // originalPush.call(arguments); - // var event = arguments[0]; - // if (!event.event || event.event.substring(0, 4) === "gtm.") { - // return - // } - // var existingStoredEvents = JSON.parse(sessionStorage.getItem("gtmDatalayerDebugger")) || []; - // existingStoredEvents.push(event); - // sessionStorage.setItem("gtmDatalayerDebugger", JSON.stringify(existingStoredEvents)); - // $(function() { - // renderItems(existingStoredEvents); - // }); - // } - - -})(jQuery); + $('#gtm-ecommerce-woo-event-inspector').show(); + }); +})(jQuery, window); diff --git a/gtm-ecommerce-woo.php b/gtm-ecommerce-woo.php index d01c5b1..a50d2f4 100644 --- a/gtm-ecommerce-woo.php +++ b/gtm-ecommerce-woo.php @@ -1,6 +1,6 @@ getGtmSnippetService()->initialize(); $container->getEventStrategiesService()->initialize(); $container->getEventInspectorService()->initialize(); +$container->getProductFeedService()->initialize(); $pluginService = $container->getPluginService(); $pluginService->initialize(); diff --git a/readme.txt b/readme.txt index 3025144..da68a1b 100644 --- a/readme.txt +++ b/readme.txt @@ -1,4 +1,4 @@ -=== GTM for WooCommerce FREE - Google Tag Manager Integration === +=== Tag Pilot FREE - Google Tag Manager Integration for WooCommerce === Contributors: Tag Concierge Tags: google tag manager, GA4, ecommerce events, Google Analytics, Facebook Pixel, Microsoft UET, consent mode Requires at least: 5.1.0 @@ -8,7 +8,7 @@ Stable tag: 1.10.35 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html -Complete Google Tag Manager plugin, Consent Mode v2 and server-side GTM ready. Quick install for GA4 and Facebook Pixel. +Complete Google Tag Manager plugin for WooCommerce, Consent Mode v2 and Server-Side GTM ready. Ready setup for GA4 and Facebook Pixel. Built-in product feeds for Google Ads and Facebook. == Description == diff --git a/src/Container.php b/src/Container.php index a986c48..c62a9ca 100644 --- a/src/Container.php +++ b/src/Container.php @@ -8,6 +8,7 @@ use GtmEcommerceWoo\Lib\Service\SettingsService; use GtmEcommerceWoo\Lib\Service\PluginService; use GtmEcommerceWoo\Lib\Service\EventInspectorService; +use GtmEcommerceWoo\Lib\Service\ProductFeedService; use GtmEcommerceWoo\Lib\Util\WpSettingsUtil; use GtmEcommerceWoo\Lib\Util\WcOutputUtil; use GtmEcommerceWoo\Lib\Util\WcTransformerUtil; @@ -45,8 +46,13 @@ public function __construct( string $pluginVersion ) { 'add_billing_info', 'add_payment_info', 'add_shipping_info', + 'add_to_wishlist', + 'remove_from_wishlist', 'abandon_cart', 'abandon_checkout', + 'language', + 'change_language', + 'change_currency' ]; $serverEvents = [ // 'add_to_cart', @@ -75,6 +81,7 @@ public function __construct( string $pluginVersion ) { $this->settingsService = new SettingsService($wpSettingsUtil, $events, $proEvents, $serverEvents, $tagConciergeApiUrl, $pluginVersion); $this->pluginService = new PluginService($spineCaseNamespace, $wpSettingsUtil, $wcOutputUtil, $pluginVersion); $this->eventInspectorService = new EventInspectorService($wpSettingsUtil, $wcOutputUtil); + $this->productFeedService = new ProductFeedService($snakeCaseNamespace, $wpSettingsUtil); } public function getSettingsService(): SettingsService { @@ -97,6 +104,10 @@ public function getEventInspectorService(): EventInspectorService { return $this->eventInspectorService; } + public function getProductFeedService(): ProductFeedService { + return $this->productFeedService; + } + public function getWcTransformerUtil() { return $this->wcTransformerUtil; } diff --git a/src/Service/EventInspectorService.php b/src/Service/EventInspectorService.php index 5049293..148ac17 100644 --- a/src/Service/EventInspectorService.php +++ b/src/Service/EventInspectorService.php @@ -34,10 +34,31 @@ public function initialize() { } } - add_action( 'wp_enqueue_scripts', [$this, 'enqueueScript'], 0 ); + $this->wcOutputUtil->scriptFile('gtm-ecommerce-woo-event-inspector', ['jquery']); + $this->wcOutputUtil->cssFile('gtm-ecommerce-woo-event-inspector'); + + add_action( 'wp_enqueue_scripts', [$this, 'wpEnqueueScripts'] ); add_action( 'wp_footer', [$this, 'footerHtml'], 0 ); } + public function wpEnqueueScripts() { + wp_enqueue_script( + 'highlight.js', + 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js' + ); + + wp_enqueue_script( + 'highlight.js-json', + 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/json.min.js', + ['highlight.js'] + ); + + wp_enqueue_style( + 'highlight.js', + 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css' + ); + } + public function isDisabled(): bool { if ($this->wpSettingsUtil->getOption('event_inspector_enabled') === 'yes-admin') { $user = \wp_get_current_user(); @@ -51,25 +72,37 @@ public function isDisabled(): bool { return false; } - public function enqueueScript() { - if ($this->isDisabled()) { - return; - } - $this->wcOutputUtil->scriptFile('gtm-ecommerce-woo-event-inspector', ['jquery']); - } - - public function footerHtml() { if ($this->isDisabled()) { return; } ?> -
-
Start shopping (add to cart, purchase) to see eCommerce events below, click event to see details.
Those events can be forwarded to number of tools in GTM. See documentation for details.
- [ + 'id', + 'title', + 'description', + 'price', + 'condition', + 'link', + 'availability', + 'image_link', + 'item_group_id' + ] + ]; + protected $defaultSchedule = [ + 'google' => '00:00:00' + ]; + + + public function __construct( string $snakeCaseNamespace, WpSettingsUtil $wpSettingsUtil ) { + $this->snakeCaseNamespace = $snakeCaseNamespace; + $this->wpSettingsUtil = $wpSettingsUtil; + } + + public function initialize() { + add_filter( 'cron_schedules', [$this, 'schedules']); + + $cronName = $this->snakeCaseNamespace . '_product_feed'; + + if (false) { + $timestamp = wp_next_scheduled( $cronName ); + wp_unschedule_event( $timestamp, $cronName ); + return; + } + + add_action( $cronName, [$this, 'cronJob'] ); + if ( ! wp_next_scheduled( $cronName ) ) { + wp_schedule_event( time(), 'minutely', $cronName ); + } + } + + public function schedules( $schedules ) { + $schedules['minutely'] = array( + 'interval' => 60, + 'display' => __('Once a minute') + ); + return $schedules; + } + + public function generateRandomString() { + return bin2hex(random_bytes(16)); + } + + public function getProductFeedFile($type) { + $fileName = $this->wpSettingsUtil->getOption('product_feed_' . $type . '_file_name'); + if ($fileName === false) { + $fileName = $this->generateRandomString() . '_product_feed_' . $type . '.tsv'; + $this->wpSettingsUtil->updateOption('product_feed_' . $type . '_file_name', $fileName); + } + $upload_dir = wp_upload_dir(); + $this->wpSettingsUtil->updateOption('product_feed_' . $type . '_file_url', $upload_dir['baseurl'] . '/' . $fileName); + return $upload_dir['basedir'] . '/' . $fileName; + } + + public function getProductFeedTempFile($type) { + return $this->getProductFeedFile($type) . '.tmp'; + } + + protected function shouldGenerateNewFeed($type) + { + $enabled = $this->wpSettingsUtil->getOption('product_feed_' . $type . '_enabled'); + + if ($enabled !== '1') { + return false; + } + + $lastStarted = get_transient($this->snakeCaseNamespace . '_product_feed_' . $type . '_started'); + + + // Check if we already generated today + if ($lastStarted !== false && date('Ymd') === date('Ymd', $lastStarted)) { + // Check if it's scheduled time + $scheduleTime = $this->wpSettingsUtil->getOption('product_feed_' . $type . '_schedule') + ?? $this->defaultSchedule[$type]; + + $scheduledDateTime = strtotime(date('Y-m-d') . ' ' . $scheduleTime); + if (time() >= $scheduledDateTime && $lastStarted < $scheduledDateTime) { + return true; + } + return false; + } + + return true; + } + + public function cronJob() { + $timeout = 30; // seconds + $startTime = time(); + + foreach ($this->types as $type) { + + while ( time() - $startTime <= $timeout ) { + // Check if there's an ongoing generation + $currentPage = get_transient($this->snakeCaseNamespace . '_product_feed_' . $type . '_current_page'); + + if ($currentPage === false) { + // No ongoing generation, check if we should start new one + if (!$this->shouldGenerateNewFeed($type)) { + break; + } + + // Start new generation + $currentPage = 1; + $tempFile = $this->getProductFeedTempFile($type); + + // Write headers + $handle = fopen($tempFile, 'w'); + fputcsv($handle, $this->headers[$type], "\t"); + fclose($handle); + + set_transient($this->snakeCaseNamespace . '_product_feed_' . $type . '_started', time()); + } + + $nextPage = $this->generateProductFeed($type, $currentPage); + + if ($nextPage === false) { + // Generation completed + $tempFile = $this->getProductFeedTempFile($type); + $finalFile = $this->getProductFeedFile($type); + rename($tempFile, $finalFile); + + delete_transient($this->snakeCaseNamespace . '_product_feed_' . $type . '_current_page'); + set_transient($this->snakeCaseNamespace . '_product_feed_' . $type . '_generated', time()); + break; + } + + $currentPage = $nextPage; + set_transient($this->snakeCaseNamespace . '_product_feed_' . $type . '_current_page', $currentPage); + } + } + } + + /** + * Returns another page number 2, 3, 4 if there is more products to process + * or false if it finished + */ + function generateProductFeed($type, $page = 1) { + $query = new \WC_Product_Query( array( + 'status' => 'publish', + 'page' => 1, + 'paginate' => true, + 'limit' => 100, + 'orderby' => 'ID', + 'order' => 'ASC', + 'visibility' => 'visible', + ) ); + + $results = $query->get_products(); + if (empty($results->products)) { + return false; + } + + $tempFile = $this->getProductFeedTempFile($type); + $handle = fopen($tempFile, 'a'); + + foreach ($results->products as $product) { + + if ($product->is_type('variable')) { + foreach ($product->get_available_variations('objects') as $variant) { + $data = $this->formatProductData($variant, $type); + fputcsv($handle, $data, "\t"); + } + } else { + $data = $this->formatProductData($product, $type); + fputcsv($handle, $data, "\t"); + } + } + + fclose($handle); + + if ($results->max_num_pages > $page) { + return $page + 1; + } + + return false; + } + + protected function formatProductData($product, $type) { + $data = []; + foreach ($this->headers[$type] as $header) { + switch ($header) { + case 'id': + $data[] = $product->get_sku(); + break; + case 'title': + $data[] = $product->get_name(); + break; + case 'description': + $data[] = substr(wp_strip_all_tags($product->get_description()), 0, 5000); + break; + case 'price': + $data[] = $product->get_price(); + break; + case 'condition': + $data[] = 'new'; + break; + case 'link': + $data[] = get_permalink($product->get_id()); + break; + case 'availability': + $data[] = $product->is_in_stock() ? 'in stock' : 'out of stock'; + break; + case 'image_link': + $data[] = wp_get_attachment_url($product->get_image_id()); + break; + case 'item_group_id': + $parentProduct = wc_get_product($product->get_parent_id()); + $data[] = $parentProduct ? $parentProduct->get_sku() : ''; + break; + default: + $data[] = ''; + } + } + return $data; + } + + +} diff --git a/src/Service/SettingsService.php b/src/Service/SettingsService.php index cf6e719..931d1dd 100644 --- a/src/Service/SettingsService.php +++ b/src/Service/SettingsService.php @@ -33,6 +33,9 @@ class SettingsService { /** @var false */ protected $allowServerTracking = false; + /** @var false */ + protected $isPro = false; + /** @var string */ protected $filter = 'basic'; @@ -184,7 +187,7 @@ public function settingsInit() { 'Google Tag Manager presets', 'It\'s time to define what to do with tracked eCommerce events. We know that settings up GTM workspace may be cumbersome. That\'s why the plugin comes with a set of presets you can import to your GTM workspace to create all required Tags, Triggers and Variables. Select a preset in dropdown below, download the JSON file and import it in Admin panel in your GTM workspace, see plugin Documentation for details):


', + margin-left: 4%; width: 45%" class="postbox">

Google Analytics 4

Description

Supported events: 2

Download

Version: N/A


', 'gtm_presets' ); @@ -208,7 +211,7 @@ public function settingsInit() { 'Event Deferral', 'Defer events pushed to data layer until "DOM ready" event. Useful when using asynchronous Consent Management Platform to ensure consent state is provided before firing any events.', 'tools', - [ 'grid' => 'item', 'badge' => 'PRO' ] + [ 'grid' => 'item', 'badge' => $this->isPro ? 'PRO' : null ] ); $this->wpSettingsUtil->addSettingsSection( @@ -230,18 +233,18 @@ public function settingsInit() { $this->wpSettingsUtil->addSettingsSection( 'product_feed_google', 'Google Product Feed', - '', + 'Generates a public CSV file with all product data (published and visible products and their variants) that can be loaded to Google Merchant Center to populate product catalog. The file URL will be available shortly after enabling.

URL:' . ($this->wpSettingsUtil->getOption('product_feed_google_file_url') ? '' : 'Pending' ), 'tools', - [ 'grid' => 'item' ] + [ 'grid' => 'end' ] ); - $this->wpSettingsUtil->addSettingsSection( + /*$this->wpSettingsUtil->addSettingsSection( 'product_feed_facebook', 'Facebook Product Feed', '', 'tools', - [ 'grid' => 'end' ] - ); + [ 'grid' => 'end', 'badge' => 'PRO' ] + );*/ $this->wpSettingsUtil->addSettingsField( 'disabled', @@ -344,16 +347,16 @@ public function settingsInit() { 'Enable Product Feed', [$this, 'checkboxField'], 'product_feed_google', - 'Generate Product Feed for Google Merchant Center.' + 'When enabled the public file will be created and updated every day.' ); - $this->wpSettingsUtil->addSettingsField( + /*$this->wpSettingsUtil->addSettingsField( 'product_feed_facebook_enabled', 'Enable Product Feed', [$this, 'checkboxField'], 'product_feed_facebook', 'Generate Product Feed for Facebook / Meta Product Catalog.' - ); + );*/ $this->wpSettingsUtil->addSettingsField( 'gtm_snippet_prevent_load', @@ -541,7 +544,7 @@ class="large-text code" public function optionsPage() { $this->wpSettingsUtil->addSubmenuPage( 'options-general.php', - $this->allowServerTracking ? 'Google Tag Manager for WooCommerce PRO' : 'Google Tag Manager for WooCommerce FREE', + $this->allowServerTracking ? 'Google Tag Manager for WooCommerce PRO' : 'Tag Pilot - Google Tag Manager for WooCommerce FREE', 'Google Tag Manager', 'manage_options' ); diff --git a/src/Util/SanitizationUtil.php b/src/Util/SanitizationUtil.php index 9f98b75..7b633fa 100644 --- a/src/Util/SanitizationUtil.php +++ b/src/Util/SanitizationUtil.php @@ -13,6 +13,10 @@ class SanitizationUtil { 'style' => [], 'data-target' => [], ], + 'input' => [ + 'type' => [], + 'value' => [] + ], 'br' => [], 'div' => [ 'id' => [], diff --git a/src/Util/WcOutputUtil.php b/src/Util/WcOutputUtil.php index 3206b34..505e20b 100644 --- a/src/Util/WcOutputUtil.php +++ b/src/Util/WcOutputUtil.php @@ -7,11 +7,13 @@ class WcOutputUtil { protected $pluginVersion = ''; protected $scripts = []; protected $scriptFiles = []; + protected $cssFiles = []; public function __construct( $pluginVersion) { $this->pluginVersion = $pluginVersion; add_action( 'wp_footer', [$this, 'wpFooter'], 20 ); add_action( 'wp_enqueue_scripts', [$this, 'wpEnqueueScripts'] ); + add_action( 'wp_enqueue_scripts', [$this, 'wpEnqueueStyles'] ); add_filter( 'safe_style_css', function( $styles ) { $styles[] = 'display'; @@ -59,6 +61,13 @@ public function scriptFile( $scriptFileName, $scriptFileDeps = [], $scriptFileFo ]; } + public function cssFile( $cssFileName, $cssFileDeps = [] ) { + $this->cssFiles[] = [ + 'name' => $cssFileName, + 'deps' => $cssFileDeps + ]; + } + public function wpEnqueueScripts() { foreach ($this->scriptFiles as $scriptFile) { wp_enqueue_script( @@ -70,4 +79,15 @@ public function wpEnqueueScripts() { ); } } + + public function wpEnqueueStyles() { + foreach ($this->cssFiles as $cssFile) { + wp_enqueue_style( + $cssFile['name'], + plugin_dir_url( dirname( $this->pluginDir ) ) . 'assets/' . $cssFile['name'] . '.css', + $cssFile['deps'], + $this->pluginVersion + ); + } + } } From 8867fa0b4ad2425a2d2db576bce82f23b6525cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20R=C4=85czka?= Date: Sun, 10 Nov 2024 21:46:21 +0100 Subject: [PATCH 03/14] Slight rework of the settings, phpcs --- readme.txt | 6 ++ src/Service/EventInspectorService.php | 33 +++++------ src/Service/ProductFeedService.php | 23 ++++---- src/Service/SettingsService.php | 79 ++++++++++++++++++++------- src/Util/SanitizationUtil.php | 3 +- src/Util/WpSettingsUtil.php | 6 +- 6 files changed, 98 insertions(+), 52 deletions(-) diff --git a/readme.txt b/readme.txt index da68a1b..fb8d067 100644 --- a/readme.txt +++ b/readme.txt @@ -162,6 +162,12 @@ Yes! Just use the appropriate preset available in the settings screen of the plu == Changelog == += 1.11.0 = + +* added product feed tool +* introduced tools gallery +* improved styling of events inspector + = 1.10.34 = * removed unnecessary code diff --git a/src/Service/EventInspectorService.php b/src/Service/EventInspectorService.php index 148ac17..bb9644c 100644 --- a/src/Service/EventInspectorService.php +++ b/src/Service/EventInspectorService.php @@ -80,29 +80,30 @@ public function footerHtml() { */ ?> -
-
+
Events
+
Consent
+
Cookies
+
HTTP Calls
+
*/ + ?> +
+
    -
    - -
    -
    -
    +
    + +
    +
    + wpSettingsUtil->getOption('product_feed_' . $type . '_file_name'); - if ($fileName === false) { + if (false === $fileName) { $fileName = $this->generateRandomString() . '_product_feed_' . $type . '.tsv'; $this->wpSettingsUtil->updateOption('product_feed_' . $type . '_file_name', $fileName); } @@ -77,15 +77,14 @@ public function getProductFeedFile($type) { return $upload_dir['basedir'] . '/' . $fileName; } - public function getProductFeedTempFile($type) { + public function getProductFeedTempFile( $type) { return $this->getProductFeedFile($type) . '.tmp'; } - protected function shouldGenerateNewFeed($type) - { + protected function shouldGenerateNewFeed( $type) { $enabled = $this->wpSettingsUtil->getOption('product_feed_' . $type . '_enabled'); - if ($enabled !== '1') { + if ('1' !== $enabled) { return false; } @@ -93,12 +92,12 @@ protected function shouldGenerateNewFeed($type) // Check if we already generated today - if ($lastStarted !== false && date('Ymd') === date('Ymd', $lastStarted)) { + if (false !== $lastStarted && gmdate('Ymd') === gmdate('Ymd', $lastStarted)) { // Check if it's scheduled time $scheduleTime = $this->wpSettingsUtil->getOption('product_feed_' . $type . '_schedule') ?? $this->defaultSchedule[$type]; - $scheduledDateTime = strtotime(date('Y-m-d') . ' ' . $scheduleTime); + $scheduledDateTime = strtotime(gmdate('Y-m-d') . ' ' . $scheduleTime); if (time() >= $scheduledDateTime && $lastStarted < $scheduledDateTime) { return true; } @@ -118,7 +117,7 @@ public function cronJob() { // Check if there's an ongoing generation $currentPage = get_transient($this->snakeCaseNamespace . '_product_feed_' . $type . '_current_page'); - if ($currentPage === false) { + if (false === $currentPage) { // No ongoing generation, check if we should start new one if (!$this->shouldGenerateNewFeed($type)) { break; @@ -138,7 +137,7 @@ public function cronJob() { $nextPage = $this->generateProductFeed($type, $currentPage); - if ($nextPage === false) { + if (false === $nextPage) { // Generation completed $tempFile = $this->getProductFeedTempFile($type); $finalFile = $this->getProductFeedFile($type); @@ -159,7 +158,7 @@ public function cronJob() { * Returns another page number 2, 3, 4 if there is more products to process * or false if it finished */ - function generateProductFeed($type, $page = 1) { + public function generateProductFeed( $type, $page = 1) { $query = new \WC_Product_Query( array( 'status' => 'publish', 'page' => 1, @@ -200,7 +199,7 @@ function generateProductFeed($type, $page = 1) { return false; } - protected function formatProductData($product, $type) { + protected function formatProductData( $product, $type) { $data = []; foreach ($this->headers[$type] as $header) { switch ($header) { diff --git a/src/Service/SettingsService.php b/src/Service/SettingsService.php index 931d1dd..d1216da 100644 --- a/src/Service/SettingsService.php +++ b/src/Service/SettingsService.php @@ -68,6 +68,11 @@ public function initialize() { 'Tools' ); + $this->wpSettingsUtil->addTab( + 'consent_mode', + 'Consent Mode' + ); + $this->wpSettingsUtil->addTab( 'gtm_server', 'GTM Server-Side' @@ -206,14 +211,6 @@ public function settingsInit() { [ 'grid' => 'start' ] ); - $this->wpSettingsUtil->addSettingsSection( - 'event_deferral', - 'Event Deferral', - 'Defer events pushed to data layer until "DOM ready" event. Useful when using asynchronous Consent Management Platform to ensure consent state is provided before firing any events.', - 'tools', - [ 'grid' => 'item', 'badge' => $this->isPro ? 'PRO' : null ] - ); - $this->wpSettingsUtil->addSettingsSection( 'cart_data_gads', 'Google Ads Cart Data', @@ -222,22 +219,46 @@ public function settingsInit() { [ 'grid' => 'item', 'badge' => 'PRO' ] ); - $this->wpSettingsUtil->addSettingsSection( - 'server_side_endpoint_cogs', - 'sGTM COGS Endpoint', - 'When using server-side GTM you can make additional transformation before data is sent to the 3rd party platform. This endpoint allows replacing default revenue based conversion values with profit information stored in WooCommerce using COGS plugin.', - 'tools', - [ 'grid' => 'item', 'badge' => 'PRO' ] - ); - $this->wpSettingsUtil->addSettingsSection( 'product_feed_google', 'Google Product Feed', - 'Generates a public CSV file with all product data (published and visible products and their variants) that can be loaded to Google Merchant Center to populate product catalog. The file URL will be available shortly after enabling.

    URL:' . ($this->wpSettingsUtil->getOption('product_feed_google_file_url') ? '' : 'Pending' ), + 'Generates a public CSV file with all product data (published and visible products and their variants) that can be loaded to Google Merchant Center to populate product catalog. The file URL will be available shortly after enabling.

    URL:' . ( $this->wpSettingsUtil->getOption('product_feed_google_file_url') ? '' : 'Pending' ), 'tools', [ 'grid' => 'end' ] ); + $this->wpSettingsUtil->addSettingsSection( + 'consent_mode_default', + 'Default Consent Mode', + 'Correct Google Consent Mode v2 requires setting default consent state before GTM installation snippet. No all Consent Management Platforms can achieve that, use the settings below to get it loaded before gtm.js.', + 'consent_mode', + [ 'grid' => 'start' ] + ); + + $this->wpSettingsUtil->addSettingsSection( + 'event_deferral', + 'Event Deferral', + 'Defer events pushed to data layer until "DOM ready" event. Useful when using asynchronous Consent Management Platform to ensure consent state is provided before firing any events.', + 'consent_mode', + [ 'grid' => 'end', 'badge' => $this->isPro ? null : 'PRO' ] + ); + + $this->wpSettingsUtil->addSettingsSection( + 'cookies_storage', + 'Cookies Storage', + 'When using server-side webhooks having access to ad cookies is required.', + 'gtm_server', + [ 'grid' => 'start', 'badge' => 'PRO' ] + ); + + $this->wpSettingsUtil->addSettingsSection( + 'server_side_endpoint_cogs', + 'sGTM COGS Endpoint', + 'When using server-side GTM you can make additional transformation before data is sent to the 3rd party platform. This endpoint allows replacing default revenue based conversion values with profit information stored in WooCommerce using COGS plugin.', + 'gtm_server', + [ 'grid' => 'end', 'badge' => 'PRO' ] + ); + /*$this->wpSettingsUtil->addSettingsSection( 'product_feed_facebook', 'Facebook Product Feed', @@ -288,6 +309,14 @@ public function settingsInit() { ['disabled' => true, 'title' => 'Upgrade to PRO version above.'] ); + $this->wpSettingsUtil->addSettingsField( + 'consent_mode_default_enabled', + 'Enable', + [$this, 'checkboxField'], + 'consent_mode_default', + 'When clicked default consent state will be loaded before gtm.js' + ); + $this->wpSettingsUtil->addSettingsField( 'google_business_vertical', 'Google Business Vertical', @@ -296,12 +325,13 @@ public function settingsInit() { 'Select required parameter learn more here.', [ 'options' => [ - 'no' => 'Feature Disabled', + 'no' => 'None', 'retail' => 'Retail', - 'education' => 'education' + 'education' => 'Education' ], 'disabled' => true, - 'title' => 'Upgrade to PRO version above.'] + 'title' => 'Upgrade to PRO version above.' +] ); $this->wpSettingsUtil->addSettingsField( @@ -421,6 +451,15 @@ public function settingsInit() { ['type' => 'text', 'placeholder' => 'header value', 'disabled' => !$this->allowServerTracking] ); + $this->wpSettingsUtil->addSettingsField( + 'cookies_storage', + 'Cookies Storage', + [$this, 'checkboxField'], + 'cookies_storage', + 'In order to connect web events with server events identifiers from cookies can be stored on the server and added to server events.', + ['disabled' => false, 'title' => 'Checking will start storing cookies'] + ); + foreach ($this->events as $eventName) { $this->wpSettingsUtil->addSettingsField( 'event_' . $eventName, diff --git a/src/Util/SanitizationUtil.php b/src/Util/SanitizationUtil.php index 7b633fa..3cf30c5 100644 --- a/src/Util/SanitizationUtil.php +++ b/src/Util/SanitizationUtil.php @@ -15,7 +15,8 @@ class SanitizationUtil { ], 'input' => [ 'type' => [], - 'value' => [] + 'value' => [], + 'style' => [], ], 'br' => [], 'div' => [ diff --git a/src/Util/WpSettingsUtil.php b/src/Util/WpSettingsUtil.php index 509fcc6..348a517 100644 --- a/src/Util/WpSettingsUtil.php +++ b/src/Util/WpSettingsUtil.php @@ -57,15 +57,15 @@ public function addSettingsSection( $sectionName, $sectionTitle, $description, $ $grid = isset($extra['grid']) ? $extra['grid'] : null; $badge = isset($extra['badge']) ? $extra['badge'] : null; - if ($grid === 'start' || $grid === 'single') { + if ( 'start' === $grid || 'single' === $grid ) { $args['before_section'] = '
    '; } - if ($grid !== null) { + if ( null !== $grid ) { $args['before_section'] .= '
    '; $args['after_section'] = '
    '; } - if ($grid === 'end' || $grid === 'single') { + if ( 'end' === $grid || 'single' === $grid ) { $args['after_section'] .= '

    '; } From 2da764f27997c2cd3001386c15405d556b329c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20R=C4=85czka?= Date: Sun, 10 Nov 2024 22:53:56 +0100 Subject: [PATCH 04/14] Prepare settings for PRO --- src/Service/SettingsService.php | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/Service/SettingsService.php b/src/Service/SettingsService.php index d1216da..6f0ea22 100644 --- a/src/Service/SettingsService.php +++ b/src/Service/SettingsService.php @@ -216,7 +216,7 @@ public function settingsInit() { 'Google Ads Cart Data', 'Pass additional Cart Data parameters to your Google Ads Conversions. This allows to use Dynamic Remarketing campaigns or track conversion value based on COGS information provided in your Google Merchant Center Account.', 'tools', - [ 'grid' => 'item', 'badge' => 'PRO' ] + [ 'grid' => 'item', 'badge' => $this->isPro ? null : 'PRO' ] ); $this->wpSettingsUtil->addSettingsSection( @@ -248,7 +248,7 @@ public function settingsInit() { 'Cookies Storage', 'When using server-side webhooks having access to ad cookies is required.', 'gtm_server', - [ 'grid' => 'start', 'badge' => 'PRO' ] + [ 'grid' => 'start', 'badge' => $this->isPro ? null : 'PRO' ] ); $this->wpSettingsUtil->addSettingsSection( @@ -256,7 +256,7 @@ public function settingsInit() { 'sGTM COGS Endpoint', 'When using server-side GTM you can make additional transformation before data is sent to the 3rd party platform. This endpoint allows replacing default revenue based conversion values with profit information stored in WooCommerce using COGS plugin.', 'gtm_server', - [ 'grid' => 'end', 'badge' => 'PRO' ] + [ 'grid' => 'end', 'badge' => $this->isPro ? null : 'PRO' ] ); /*$this->wpSettingsUtil->addSettingsSection( @@ -280,8 +280,8 @@ public function settingsInit() { 'Track user id?', [$this, 'checkboxField'], 'basic', - $this->allowServerTracking ? 'When checked the plugin will send logged client id to dataLayer.' : 'Upgrade to PRO to track user id.', - ['disabled' => !$this->allowServerTracking, 'title' => $this->allowServerTracking ? '' : 'Upgrade to PRO to use user tracking'] + $this->isPro ? 'When checked the plugin will send logged client id to dataLayer.' : 'Upgrade to PRO to track user id.', + ['disabled' => !$this->isPro, 'title' => $this->isPro ? '' : 'Upgrade to PRO to use user tracking'] ); $this->wpSettingsUtil->addSettingsField( @@ -306,7 +306,7 @@ public function settingsInit() { [$this, 'checkboxField'], 'event_deferral', 'When clicked Ecommerce events will be pushed to DataLayer after DOM Ready event.', - ['disabled' => true, 'title' => 'Upgrade to PRO version above.'] + ['disabled' => !$this->isPro, 'title' => 'Upgrade to PRO version above.'] ); $this->wpSettingsUtil->addSettingsField( @@ -329,9 +329,8 @@ public function settingsInit() { 'retail' => 'Retail', 'education' => 'Education' ], - 'disabled' => true, - 'title' => 'Upgrade to PRO version above.' -] + 'disabled' => !$this->isPro, + ] ); $this->wpSettingsUtil->addSettingsField( @@ -342,8 +341,7 @@ public function settingsInit() { 'Optional prefix to match your product feed in Google Merchant Center, e.g. `woocommerce_gpf_`.', [ 'type' => 'text', - 'disabled' => true, - 'title' => 'Upgrade to PRO version above.' + 'disabled' => !$this->isPro ] ); @@ -355,8 +353,7 @@ public function settingsInit() { 'The Merchant Center ID. Provide this parameter if you submit an item in several Merchant Center accounts and you want to control from which Merchant Center the item’s data, for example, its COGS, should be read.', [ 'type' => 'text', - 'disabled' => true, - 'title' => 'Upgrade to PRO version above.' + 'disabled' => !$this->isPro ] ); @@ -367,8 +364,7 @@ public function settingsInit() { 'server_side_endpoint_cogs', 'Enable serving COGS information for sGTM container.', [ - 'disabled' => true, - 'title' => 'Upgrade to PRO version above.' + 'disabled' => !$this->isPro ] ); @@ -572,7 +568,7 @@ class="large-text code" disabled="disabled" value="" - placeholder="" + placeholder="" name="" />

    @@ -583,7 +579,7 @@ class="large-text code" public function optionsPage() { $this->wpSettingsUtil->addSubmenuPage( 'options-general.php', - $this->allowServerTracking ? 'Google Tag Manager for WooCommerce PRO' : 'Tag Pilot - Google Tag Manager for WooCommerce FREE', + $this->isPro ? 'Google Tag Manager for WooCommerce PRO' : 'Tag Pilot - Google Tag Manager for WooCommerce FREE', 'Google Tag Manager', 'manage_options' ); From af170ddf50c9948f0a23e3fdc9752b0dbc933d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20R=C4=85czka?= Date: Sun, 10 Nov 2024 23:00:50 +0100 Subject: [PATCH 05/14] Prepare settings for PRO --- src/Container.php | 2 ++ src/Service/SettingsService.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Container.php b/src/Container.php index c62a9ca..4873598 100644 --- a/src/Container.php +++ b/src/Container.php @@ -30,6 +30,8 @@ class Container { /** @var EventInspectorService */ public $eventInspectorService; + public $productFeedService; + /** @var WcTransformerUtil */ protected $wcTransformerUtil; diff --git a/src/Service/SettingsService.php b/src/Service/SettingsService.php index 6f0ea22..fdd1e9c 100644 --- a/src/Service/SettingsService.php +++ b/src/Service/SettingsService.php @@ -222,7 +222,7 @@ public function settingsInit() { $this->wpSettingsUtil->addSettingsSection( 'product_feed_google', 'Google Product Feed', - 'Generates a public CSV file with all product data (published and visible products and their variants) that can be loaded to Google Merchant Center to populate product catalog. The file URL will be available shortly after enabling.

    URL:' . ( $this->wpSettingsUtil->getOption('product_feed_google_file_url') ? '' : 'Pending' ), + 'Generates a public CSV file with all product data (published and visible products and their variants) that can be loaded to Google Merchant Center to populate product catalog. The file URL will be available shortly after enabling.

    URL:' . ( $this->wpSettingsUtil->getOption('product_feed_google_file_url') ? '' : ' Pending' ), 'tools', [ 'grid' => 'end' ] ); From 31a29b00e26c1ae082d63c83b4a5e7c663e55aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20R=C4=85czka?= Date: Mon, 11 Nov 2024 16:17:23 +0100 Subject: [PATCH 06/14] Inspector fixes and full settings gallery, free features --- assets/gtm-ecommerce-woo-event-inspector.css | 4 + assets/gtm-ecommerce-woo-event-inspector.js | 6 +- src/Service/GtmSnippetService.php | 87 ++++++++- src/Service/ProductFeedService.php | 27 ++- src/Service/SettingsService.php | 188 ++++++++++++++++++- 5 files changed, 295 insertions(+), 17 deletions(-) diff --git a/assets/gtm-ecommerce-woo-event-inspector.css b/assets/gtm-ecommerce-woo-event-inspector.css index d2e7fe8..3b89892 100644 --- a/assets/gtm-ecommerce-woo-event-inspector.css +++ b/assets/gtm-ecommerce-woo-event-inspector.css @@ -127,6 +127,10 @@ color: #202124; } +#gtm-ecommerce-woo-event-inspector code { + background: none; +} + #gtm-ecommerce-woo-event-inspector h3 { color: #202124; font-weight: 500; diff --git a/assets/gtm-ecommerce-woo-event-inspector.js b/assets/gtm-ecommerce-woo-event-inspector.js index a9b782b..3a6f962 100644 --- a/assets/gtm-ecommerce-woo-event-inspector.js +++ b/assets/gtm-ecommerce-woo-event-inspector.js @@ -59,9 +59,10 @@ return template .replace('{{event}}', eventName) - .replace('{{json}}', JSON.stringify(item, null, 2)); + .replace('{{json}}', hljs.highlight(JSON.stringify(item, null, 2), { language: 'json' }).value); }); $("#gtm-ecommerce-woo-event-inspector-list").html(rendered); + } $(function() { @@ -88,9 +89,6 @@ renderItems(existingStoredEvents); setInterval(checkDataLayer, 100); - if (w.hljs) { - w.hljs.highlightAll(); - } $('#gtm-ecommerce-woo-event-inspector').show(); }); diff --git a/src/Service/GtmSnippetService.php b/src/Service/GtmSnippetService.php index e8b66f2..e67bd3e 100644 --- a/src/Service/GtmSnippetService.php +++ b/src/Service/GtmSnippetService.php @@ -36,11 +36,94 @@ public function initialize() { } } + public function defaultConsentModeState() { + + $settings = array_reduce([ + 'ad_storage', + 'ad_user_data', + 'ad_personalization', + 'analytics_storage', + 'wait_for_update', + 'region', + 'url_passthrough', + 'ads_data_redaction' + ], function($agg, $settingName) { + $agg[$settingName] = $this->wpSettingsUtil->getOption('consent_mode_default_' . $settingName); + return $agg; + }, []); + extract($settings); + + $consentJs = << +window.dataLayer = window.dataLayer || []; +function gtag(){dataLayer.push(arguments);} +gtag('consent', 'default', { + 'ad_storage': '${ad_storage}', + 'ad_user_data': '${ad_user_data}', + 'ad_personalization': '${ad_personalization}', + 'analytics_storage': '${analytics_storage}' +}); + +END; + + if ($wait_for_update) { + $consentJs .= "gtag('set', 'wait_for_update', $wait_for_update);\n"; + } + + if ($region) { + $regions = explode(',', $region); + $cleanedRegions = array_map(function($r) { + return "'".trim(str_replace("'", '', $r))."'"; + }, $regions); + $regionsString = implode(',', $cleanedRegions); + $consentJs .= "gtag('set', 'region', [$regionsString]);\n"; + } + + if ('1' === $url_passthrough) { + $consentJs .= "gtag('set', 'url_passthrough', true);\n"; + } + + if ('1' === $ads_data_redaction) { + $consentJs .= "gtag('set', 'ads_data_redaction', true);\n"; + } + + $consentJs .= "\n"; + return $consentJs; + } + public function headSnippet() { - echo filter_var($this->wpSettingsUtil->getOption('gtm_snippet_head'), FILTER_FLAG_STRIP_BACKTICK) . "\n"; + + if ('1' === $this->wpSettingsUtil->getOption('consent_mode_default_enabled')) { + echo $this->defaultConsentModeState(); + } + + $snippet = filter_var($this->wpSettingsUtil->getOption('gtm_snippet_head'), FILTER_FLAG_STRIP_BACKTICK); + + if ('1' === $this->wpSettingsUtil->getOption('server_side_gtmjs_enable')) { + + $serverContainerUrl = $this->wpSettingsUtil->getOption('gtm_server_container_url'); + $domain = trim(str_replace('https://', '', $serverContainerUrl), '/'); + + $snippet = str_replace('www.googletagmanager.com', $domain, $snippet); + + } + + echo $snippet . "\n"; } public function bodySnippet() { - echo filter_var($this->wpSettingsUtil->getOption('gtm_snippet_body'), FILTER_FLAG_STRIP_BACKTICK) . "\n"; + + $snippet = filter_var($this->wpSettingsUtil->getOption('gtm_snippet_body'), FILTER_FLAG_STRIP_BACKTICK); + + if ('1' === $this->wpSettingsUtil->getOption('server_side_gtmjs_enable')) { + + $serverContainerUrl = $this->wpSettingsUtil->getOption('gtm_server_container_url'); + $domain = trim(str_replace('https://', '', $serverContainerUrl), '/'); + + $snippet = str_replace('www.googletagmanager.com', $domain, $snippet); + + } + + echo $snippet . "\n"; } } diff --git a/src/Service/ProductFeedService.php b/src/Service/ProductFeedService.php index 674a85d..8b2fa19 100644 --- a/src/Service/ProductFeedService.php +++ b/src/Service/ProductFeedService.php @@ -28,7 +28,7 @@ class ProductFeedService { ] ]; protected $defaultSchedule = [ - 'google' => '00:00:00' + 'google' => '00:30:00' ]; @@ -42,7 +42,9 @@ public function initialize() { $cronName = $this->snakeCaseNamespace . '_product_feed'; - if (false) { + + // TODO: support multiple types + if ('1' !== $this->wpSettingsUtil->getOption('product_feed_google_enabled')) { $timestamp = wp_next_scheduled( $cronName ); wp_unschedule_event( $timestamp, $cronName ); return; @@ -199,12 +201,14 @@ public function generateProductFeed( $type, $page = 1) { return false; } - protected function formatProductData( $product, $type) { + protected function formatProductData( $product, $type ) { + + $idPattern = $this->wpSettingsUtil->getOption('product_feed_' . $type . '_id_pattern', '{{sku}}'); $data = []; foreach ($this->headers[$type] as $header) { switch ($header) { case 'id': - $data[] = $product->get_sku(); + $data[] = $this->getProductId($product, $idPattern); break; case 'title': $data[] = $product->get_name(); @@ -229,7 +233,7 @@ protected function formatProductData( $product, $type) { break; case 'item_group_id': $parentProduct = wc_get_product($product->get_parent_id()); - $data[] = $parentProduct ? $parentProduct->get_sku() : ''; + $data[] = $parentProduct ? $this->getProductId($parentProduct, $idPattern) : ''; break; default: $data[] = ''; @@ -238,5 +242,18 @@ protected function formatProductData( $product, $type) { return $data; } + protected function getProductId( $product, $pattern ) { + $id = $pattern; + if (strstr($id, '{{id}}')) { + $id = str_replace('{{id}}', $product->get_id(), $id); + } + + if (strstr($id, '{{sku}}')) { + $id = str_replace('{{sku}}', $product->get_sku(), $id); + } + + return $id; + } + } diff --git a/src/Service/SettingsService.php b/src/Service/SettingsService.php index fdd1e9c..14023a0 100644 --- a/src/Service/SettingsService.php +++ b/src/Service/SettingsService.php @@ -214,7 +214,7 @@ public function settingsInit() { $this->wpSettingsUtil->addSettingsSection( 'cart_data_gads', 'Google Ads Cart Data', - 'Pass additional Cart Data parameters to your Google Ads Conversions. This allows to use Dynamic Remarketing campaigns or track conversion value based on COGS information provided in your Google Merchant Center Account.', + 'Pass additional Cart Data parameters to your Google Ads Conversions. This allows to use Dynamic Remarketing campaigns or track conversion value based on COGS information provided in your Google Merchant Center Account. Learn more.', 'tools', [ 'grid' => 'item', 'badge' => $this->isPro ? null : 'PRO' ] ); @@ -235,6 +235,16 @@ public function settingsInit() { [ 'grid' => 'start' ] ); + $this->wpSettingsUtil->addSettingsSection( + 'consent_mode_integration_complianz', + 'Complianz Integration', + 'Integrated Complianz Cookies Banner and Consent Management Platform with GTM Consent Mode v2.', + 'consent_mode', + [ 'grid' => 'item', 'badge' => $this->isPro ? null : 'PRO' ] + ); + + + $this->wpSettingsUtil->addSettingsSection( 'event_deferral', 'Event Deferral', @@ -243,12 +253,28 @@ public function settingsInit() { [ 'grid' => 'end', 'badge' => $this->isPro ? null : 'PRO' ] ); + $this->wpSettingsUtil->addSettingsSection( + 'server_side_gtmjs', + 'Load gtm.js from sGTM', + 'Modify the gtm.js installation snippet to be loaded from 1st party domain of your sGTM container.', + 'gtm_server', + [ 'grid' => 'start' ] + ); + + $this->wpSettingsUtil->addSettingsSection( + 'server_side_webhooks', + 'Server-to-server Webhook', + 'Send selected events server-to-server to improve tracking coverage.', + 'gtm_server', + [ 'grid' => 'item', 'badge' => $this->isPro ? null : 'PRO' ] + ); + $this->wpSettingsUtil->addSettingsSection( 'cookies_storage', 'Cookies Storage', 'When using server-side webhooks having access to ad cookies is required.', 'gtm_server', - [ 'grid' => 'start', 'badge' => $this->isPro ? null : 'PRO' ] + [ 'grid' => 'item', 'badge' => $this->isPro ? null : 'PRO' ] ); $this->wpSettingsUtil->addSettingsSection( @@ -317,6 +343,109 @@ public function settingsInit() { 'When clicked default consent state will be loaded before gtm.js' ); + $this->wpSettingsUtil->addSettingsField( + 'consent_mode_default_ad_storage', + 'ad_storage', + [$this, 'selectField'], + 'consent_mode_default', + 'Default state for `ad_storage` consent type.', + [ + 'options' => [ + 'denied' => 'denied', + 'granted' => 'granted' + ] + ] + ); + + $this->wpSettingsUtil->addSettingsField( + 'consent_mode_default_ad_user_data', + 'ad_user_data', + [$this, 'selectField'], + 'consent_mode_default', + 'Default state for `ad_user_data` consent type.', + [ + 'options' => [ + 'denied' => 'denied', + 'granted' => 'granted' + ] + ] + ); + + $this->wpSettingsUtil->addSettingsField( + 'consent_mode_default_ad_personalization', + 'ad_personalization', + [$this, 'selectField'], + 'consent_mode_default', + 'Default state for `ad_personalization` consent type.', + [ + 'options' => [ + 'denied' => 'denied', + 'granted' => 'granted' + ] + ] + ); + + $this->wpSettingsUtil->addSettingsField( + 'consent_mode_default_analytics_storage', + 'analytics_storage', + [$this, 'selectField'], + 'consent_mode_default', + 'Default state for `analytics_storage` consent type.', + [ + 'options' => [ + 'denied' => 'denied', + 'granted' => 'granted' + ] + ] + ); + + $this->wpSettingsUtil->addSettingsField( + 'consent_mode_default_wait_for_update', + 'wait_for_update', + [$this, 'inputField'], + 'consent_mode_default', + 'Number of ms to wait for asynchronous Consent Management Platform (banner) to load, e.g. 500', + [ + 'type' => 'number' + ] + ); + + $this->wpSettingsUtil->addSettingsField( + 'consent_mode_default_region', + 'region', + [$this, 'inputField'], + 'consent_mode_default', + 'Comma separated list of country codes, e.g. `US, FR, IT`.', + [ + 'type' => 'text' + ] + ); + + $this->wpSettingsUtil->addSettingsField( + 'consent_mode_default_url_passthrough', + 'url_passthrough', + [$this, 'checkboxField'], + 'consent_mode_default', + 'Pass through ad click, client ID, and session ID information in URLs' + ); + + $this->wpSettingsUtil->addSettingsField( + 'consent_mode_default_ads_data_redaction', + 'ads_data_redaction', + [$this, 'checkboxField'], + 'consent_mode_default', + 'Redact ads data' + ); + + $this->wpSettingsUtil->addSettingsField( + 'consent_mode_integration_complianz_enabled', + 'Enable', + [$this, 'checkboxField'], + 'consent_mode_integration_complianz', + 'When clicked consent mode update will be generated based on Complianz JS API.' + ); + + $this->wpSettingsUtil->addSettingsField( 'google_business_vertical', 'Google Business Vertical', @@ -357,6 +486,30 @@ public function settingsInit() { ] ); + $this->wpSettingsUtil->addSettingsField( + 'cart_data_google_feed_country', + 'AW Feed Country', + [$this, 'inputField'], + 'cart_data_gads', + 'The country code of the product feed to use with the integration, e.g. `US`.', + [ + 'type' => 'text', + 'disabled' => !$this->isPro + ] + ); + + $this->wpSettingsUtil->addSettingsField( + 'cart_data_google_feed_lanuage', + 'AW Feed Language', + [$this, 'inputField'], + 'cart_data_gads', + 'The lanugage code of the product feed to use with the integration, e.g. `EN`.', + [ + 'type' => 'text', + 'disabled' => !$this->isPro + ] + ); + $this->wpSettingsUtil->addSettingsField( 'server_side_endpoint_cogs_enabled', 'Enable COGS Endpoint', @@ -376,6 +529,17 @@ public function settingsInit() { 'When enabled the public file will be created and updated every day.' ); + $this->wpSettingsUtil->addSettingsField( + 'product_feed_google_id_pattern', + 'Product ID Pattern', + [$this, 'inputField'], + 'product_feed_google', + 'How Product ID should be generated. Use {{sku}} or {{id}} and any prefix/suffix.', + [ + 'type' => 'text' + ] + ); + /*$this->wpSettingsUtil->addSettingsField( 'product_feed_facebook_enabled', 'Enable Product Feed', @@ -425,7 +589,7 @@ public function settingsInit() { [$this, 'inputField'], 'gtm_server_container', 'The full url of you GTM Server Container.', - ['type' => 'text', 'placeholder' => 'https://measure.example.com', 'disabled' => !$this->allowServerTracking] + ['type' => 'text', 'placeholder' => 'https://measure.example.com'] ); @@ -433,7 +597,7 @@ public function settingsInit() { 'gtm_server_ga4_client_activation_path', 'GA4 Client Activation Path', [$this, 'inputField'], - 'gtm_server_container', + 'server_side_webhooks', 'GA4 Client Activation path as defined in GTM Client. If you are using our Presets use default value of `/mp`.', ['type' => 'text', 'placeholder' => '/mp', 'disabled' => !$this->allowServerTracking] ); @@ -442,18 +606,26 @@ public function settingsInit() { 'gtm_server_preview_header', 'X-Gtm-Server-Preview HTTP header', [$this, 'inputField'], - 'gtm_server_container', + 'server_side_webhooks', 'In order to use GTM Preview feature, paste the HTTP header from GTM Preview tool. The value will change over time.', ['type' => 'text', 'placeholder' => 'header value', 'disabled' => !$this->allowServerTracking] ); + $this->wpSettingsUtil->addSettingsField( + 'server_side_gtmjs_enable', + 'Enable', + [$this, 'checkboxField'], + 'server_side_gtmjs', + 'Will change public GTM domain to sGTM custom domain provided in the settings above.', + ); + $this->wpSettingsUtil->addSettingsField( 'cookies_storage', 'Cookies Storage', [$this, 'checkboxField'], 'cookies_storage', 'In order to connect web events with server events identifiers from cookies can be stored on the server and added to server events.', - ['disabled' => false, 'title' => 'Checking will start storing cookies'] + ['disabled' => !$this->isPro, 'title' => 'Checking will start storing cookies'] ); foreach ($this->events as $eventName) { @@ -490,6 +662,10 @@ public function settingsInit() { ['disabled' => !$this->allowServerTracking, 'title' => $this->allowServerTracking ? '' : 'Upgrade to PRO to use the beta of server-side tracking'] ); } + + if ($this->wpSettingsUtil->getOption('product_feed_google_id_pattern') === false) { + $this->wpSettingsUtil->updateOption('product_feed_google_id_pattern', '{{sku}}'); + } } public function checkboxField( $args ) { From ba332d9c3feb0acd3c4299a5c741ea46bcbb13a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20R=C4=85czka?= Date: Mon, 11 Nov 2024 16:21:03 +0100 Subject: [PATCH 07/14] Replace depracted syntax --- src/Service/GtmSnippetService.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Service/GtmSnippetService.php b/src/Service/GtmSnippetService.php index e67bd3e..6946726 100644 --- a/src/Service/GtmSnippetService.php +++ b/src/Service/GtmSnippetService.php @@ -58,10 +58,10 @@ public function defaultConsentModeState() { window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('consent', 'default', { - 'ad_storage': '${ad_storage}', - 'ad_user_data': '${ad_user_data}', - 'ad_personalization': '${ad_personalization}', - 'analytics_storage': '${analytics_storage}' + 'ad_storage': '{$ad_storage}', + 'ad_user_data': '{$ad_user_data}', + 'ad_personalization': '{$ad_personalization}', + 'analytics_storage': '{$analytics_storage}' }); END; From 02ff29c1caa01ce910ba9a355395ad5853724e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20R=C4=85czka?= Date: Mon, 11 Nov 2024 16:31:39 +0100 Subject: [PATCH 08/14] Rename settings --- src/Service/GtmSnippetService.php | 8 ++++---- src/Service/SettingsService.php | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Service/GtmSnippetService.php b/src/Service/GtmSnippetService.php index 6946726..699bcee 100644 --- a/src/Service/GtmSnippetService.php +++ b/src/Service/GtmSnippetService.php @@ -97,7 +97,7 @@ public function headSnippet() { echo $this->defaultConsentModeState(); } - $snippet = filter_var($this->wpSettingsUtil->getOption('gtm_snippet_head'), FILTER_FLAG_STRIP_BACKTICK); + $snippet = $this->wpSettingsUtil->getOption('gtm_snippet_head'); if ('1' === $this->wpSettingsUtil->getOption('server_side_gtmjs_enable')) { @@ -108,12 +108,12 @@ public function headSnippet() { } - echo $snippet . "\n"; + echo filter_var($snippet, FILTER_FLAG_STRIP_BACKTICK) . "\n"; } public function bodySnippet() { - $snippet = filter_var($this->wpSettingsUtil->getOption('gtm_snippet_body'), FILTER_FLAG_STRIP_BACKTICK); + $snippet = $this->wpSettingsUtil->getOption('gtm_snippet_body'); if ('1' === $this->wpSettingsUtil->getOption('server_side_gtmjs_enable')) { @@ -124,6 +124,6 @@ public function bodySnippet() { } - echo $snippet . "\n"; + echo filter_var($snippet, FILTER_FLAG_STRIP_BACKTICK) . "\n"; } } diff --git a/src/Service/SettingsService.php b/src/Service/SettingsService.php index 14023a0..5cefbc5 100644 --- a/src/Service/SettingsService.php +++ b/src/Service/SettingsService.php @@ -463,11 +463,11 @@ public function settingsInit() { ); $this->wpSettingsUtil->addSettingsField( - 'dynamic_remarketing_google_id_prefix', - 'Item ID prefix', + 'dynamic_remarketing_google_id_pattern', + 'Item ID Pattern', [$this, 'inputField'], 'cart_data_gads', - 'Optional prefix to match your product feed in Google Merchant Center, e.g. `woocommerce_gpf_`.', + 'Product ID pattern to match your product feed in Google Merchant Center, e.g. `woocommerce_gpf_{{sku}}`. Use {{sku}} or {{id}} and any prefix or suffix.', [ 'type' => 'text', 'disabled' => !$this->isPro From 4029ca95a3a4affcddb4e6f3c2c93ee26ae7507a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20R=C4=85czka?= Date: Mon, 11 Nov 2024 23:38:51 +0100 Subject: [PATCH 09/14] Add extra props to events and cs fixer --- src/GaEcommerceEntity/Event.php | 11 +++++++++++ src/Service/GtmSnippetService.php | 8 ++++---- src/Service/SettingsService.php | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/GaEcommerceEntity/Event.php b/src/GaEcommerceEntity/Event.php index 1a25f08..172223e 100644 --- a/src/GaEcommerceEntity/Event.php +++ b/src/GaEcommerceEntity/Event.php @@ -7,6 +7,7 @@ class Event implements \JsonSerializable { public $name; public $items; public $extraProps; + public $extraEcomProps; public $currency; public $transactionId; public $affiliation; @@ -18,6 +19,7 @@ class Event implements \JsonSerializable { public function __construct( $name ) { $this->name = $name; $this->extraProps = []; + $this->extraEcomProps = []; } public function setItems( array $items ): Event { @@ -70,6 +72,11 @@ public function setExtraProperty( string $propName, $propValue ): Event { return $this; } + public function setExtraEcomProperty( string $propName, $propValue ): Event { + $this->extraEcomProps[$propName] = $propValue; + return $this; + } + public function getValue(): float { if (null !== $this->value) { return $this->value; @@ -159,6 +166,10 @@ public function jsonSerialize(): array { $jsonEvent[$propName] = $propValue; } + foreach ($this->extraEcomProps as $propName => $propValue) { + $jsonEvent['ecommerce'][$propName] = $propValue; + } + $result = array_filter($jsonEvent, static function( $value ) { return !is_null($value) && '' !== $value; }); diff --git a/src/Service/GtmSnippetService.php b/src/Service/GtmSnippetService.php index 699bcee..3409a3d 100644 --- a/src/Service/GtmSnippetService.php +++ b/src/Service/GtmSnippetService.php @@ -47,7 +47,7 @@ public function defaultConsentModeState() { 'region', 'url_passthrough', 'ads_data_redaction' - ], function($agg, $settingName) { + ], function( $agg, $settingName ) { $agg[$settingName] = $this->wpSettingsUtil->getOption('consent_mode_default_' . $settingName); return $agg; }, []); @@ -72,8 +72,8 @@ function gtag(){dataLayer.push(arguments);} if ($region) { $regions = explode(',', $region); - $cleanedRegions = array_map(function($r) { - return "'".trim(str_replace("'", '', $r))."'"; + $cleanedRegions = array_map(function( $r ) { + return "'" . trim(str_replace("'", '', $r)) . "'"; }, $regions); $regionsString = implode(',', $cleanedRegions); $consentJs .= "gtag('set', 'region', [$regionsString]);\n"; @@ -94,7 +94,7 @@ function gtag(){dataLayer.push(arguments);} public function headSnippet() { if ('1' === $this->wpSettingsUtil->getOption('consent_mode_default_enabled')) { - echo $this->defaultConsentModeState(); + echo filter_var($this->defaultConsentModeState(), FILTER_FLAG_STRIP_BACKTICK); } $snippet = $this->wpSettingsUtil->getOption('gtm_snippet_head'); diff --git a/src/Service/SettingsService.php b/src/Service/SettingsService.php index 5cefbc5..56812f8 100644 --- a/src/Service/SettingsService.php +++ b/src/Service/SettingsService.php @@ -616,7 +616,7 @@ public function settingsInit() { 'Enable', [$this, 'checkboxField'], 'server_side_gtmjs', - 'Will change public GTM domain to sGTM custom domain provided in the settings above.', + 'Will change public GTM domain to sGTM custom domain provided in the settings above.' ); $this->wpSettingsUtil->addSettingsField( From e75e9c689f1951bda7ae63ad438a1e68c36d9a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20R=C4=85czka?= Date: Mon, 11 Nov 2024 23:57:12 +0100 Subject: [PATCH 10/14] Allow inactive settings tabs --- src/Service/SettingsService.php | 7 +++++++ src/Util/WpSettingsUtil.php | 13 ++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Service/SettingsService.php b/src/Service/SettingsService.php index 56812f8..c0da964 100644 --- a/src/Service/SettingsService.php +++ b/src/Service/SettingsService.php @@ -78,6 +78,13 @@ public function initialize() { 'GTM Server-Side' ); + $this->wpSettingsUtil->addTab( + 'gtm_server_presets', + 'GTM Server Presets' . $this->isPro ? '' : ' PRO', + false, + !$this->isPro + ); + $this->wpSettingsUtil->addTab( 'support', 'Support', diff --git a/src/Util/WpSettingsUtil.php b/src/Util/WpSettingsUtil.php index 348a517..d8345fb 100644 --- a/src/Util/WpSettingsUtil.php +++ b/src/Util/WpSettingsUtil.php @@ -36,11 +36,12 @@ public function registerSetting( $settingName) { return register_setting( $this->snakeCaseNamespace, $this->snakeCaseNamespace . '_' . $settingName ); } - public function addTab( $tabName, $tabTitle, $showSaveButton = true) { + public function addTab( $tabName, $tabTitle, $showSaveButton = true, $inactive = false) { $this->tabs[$tabName] = [ 'name' => $tabName, 'title' => $tabTitle, - 'show_save_button' => $showSaveButton + 'show_save_button' => $showSaveButton, + 'inactive' => $inactive ]; } @@ -129,8 +130,14 @@ function() use ( $capabilities, $snakeCaseNamespace, $spineCaseNamespace, $activ