/* ads-consent.js — Google Ads tag, gated behind a consent banner. Page-side only. * * Page: /odoo-black-friday/. Companion to /static/black-friday.js (which is untouched). * The tag ID and the conversion label are read from the banner element's data-* attributes * (#adsConsent[data-ads-id][data-ads-send-to]), so this file stays generic and reusable and * the page holds the one copy of the config. Without those attributes the script stays inert. * * WHY A GATE: the Google Ads tag is NOT cookieless — it sets the advertising cookies * _gcl_au / _gcl_aw. So nothing loads and no request fires until the visitor accepts. * The default is DENY. Declining genuinely prevents the tag from loading. * * WHAT THE CONVERSIONS ARE: both are CLICK PROXIES, not sales. * - a buy form on this page (POST /shop/post_basket) -> data-ads-send-to * - the free-trial form (#trialForm, the n8n webhook) -> data-ads-trial-send-to * The real purchase is measured on the checkout host (20.odoo4projects.com) by odoo-dev. * See wiki/concepts/conversion-tracking-contract. Never label either a sale. * * SAFETY: a conversion tag must never swallow an action. On submit we fire the event and run the * action from the event callback, with a hard timeout that runs it anyway. For a buy form the * action is form.submit(); for the trial form the page hands us its send function via * window.o4pAds.trialConversion. With JavaScript off the native POST forms submit untouched * (no banner, no conversion, no error) and the trial form does its native thing. */ (function () { "use strict"; var STORE_KEY = "o4p_ads_consent"; // "granted" | "denied" var SUBMIT_FALLBACK_MS = 700; // hard cap: submit even if the tag never answers var cfg = null; // { id, sendTo, trialSendTo } — from the banner, never hard-coded /* ---- the visitor's stored choice ---- */ function readChoice() { try { return window.localStorage.getItem(STORE_KEY); } catch (e) { return null; } } function saveChoice(value) { try { window.localStorage.setItem(STORE_KEY, value); } catch (e) { /* private mode / storage disabled: treat as no choice, banner stays up */ } } function isGranted() { return readChoice() === "granted"; } /* ---- cookie removal (only used to revoke a prior grant) ---- */ function clearCookie(name) { var host = window.location.hostname; var bare = host.replace(/^www\./, ""); var paths = ["/", "/odoo-black-friday/"]; var domains = ["", "; domain=" + host, "; domain=." + bare]; for (var i = 0; i < paths.length; i++) { for (var j = 0; j < domains.length; j++) { document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=" + paths[i] + domains[j]; } } } function clearAdsCookies() { clearCookie("_gcl_au"); clearCookie("_gcl_aw"); } /* ---- the tag: loaded ONLY on consent ---- */ function loadGtag() { if (!cfg || window.__o4pAdsLoaded) return; window.__o4pAdsLoaded = true; window.dataLayer = window.dataLayer || []; window.gtag = window.gtag || function () { window.dataLayer.push(arguments); }; window.gtag("js", new Date()); window.gtag("config", cfg.id); var s = document.createElement("script"); s.async = true; s.src = "https://www.googletagmanager.com/gtag/js?id=" + cfg.id; document.head.appendChild(s); } /* ---- fire one conversion, then run the action from the callback or the hard timeout ---- */ function fireConversion(sendTo, action) { var done = false; function run() { if (done) return; done = true; action(); // the action always runs: at most SUBMIT_FALLBACK_MS after the event } try { window.gtag("event", "conversion", { send_to: sendTo, event_callback: run, }); } catch (err) { run(); } window.setTimeout(run, SUBMIT_FALLBACK_MS); } /* ---- the conversion proxy, fired on a buy-form submit ---- */ /* NOTE: the buy path below is deliberately left byte-identical to what shipped and was * verified on 2026-11-02 — its label, its one mark and its 700 ms fallback are unchanged. */ function onSubmit(e) { var form = e.target; if (!form || form.tagName !== "FORM") return; var action = form.getAttribute("action") || ""; if (action.indexOf("/shop/post_basket") === -1) return; // Not configured, no consent, or no tag: do nothing, let the native POST go through. if (!cfg || !isGranted() || typeof window.gtag !== "function") return; e.preventDefault(); var submitted = false; function submitNow() { if (submitted) return; submitted = true; form.submit(); // bypasses the submit event: no loop, no re-fire } try { window.gtag("event", "conversion", { send_to: cfg.sendTo, event_callback: submitNow, }); } catch (err) { submitNow(); } // Hard fallback: a blocked / slow / failed tag must never stop the purchase. window.setTimeout(submitNow, SUBMIT_FALLBACK_MS); } /* ---- the banner ---- */ function bannerEl() { return document.getElementById("adsConsent"); } function showBanner() { var el = bannerEl(); if (el) el.hidden = false; } function hideBanner() { var el = bannerEl(); if (el) el.hidden = true; } function accept() { saveChoice("granted"); loadGtag(); hideBanner(); } function decline() { saveChoice("denied"); clearAdsCookies(); hideBanner(); } function init() { document.addEventListener("submit", onSubmit, false); var banner = bannerEl(); if (!banner) return; // Config lives in the page. Missing config -> stay inert (no tag, no conversion). var id = banner.getAttribute("data-ads-id"); var sendTo = banner.getAttribute("data-ads-send-to"); if (!id || !sendTo) return; cfg = { id: id, sendTo: sendTo, trialSendTo: banner.getAttribute("data-ads-trial-send-to"), bookSendTo: banner.getAttribute("data-ads-book-send-to"), }; var choice = readChoice(); if (choice === "granted") { loadGtag(); // returning visitor who already accepted: load straight away } else if (choice !== "denied") { showBanner(); // first visit (or no stored choice): default deny, ask } var acceptBtn = document.getElementById("adsConsentAccept"); if (acceptBtn) acceptBtn.addEventListener("click", accept); var declineBtn = document.getElementById("adsConsentDecline"); if (declineBtn) declineBtn.addEventListener("click", decline); // A real way to change the choice later. var opener = document.getElementById("adsConsentOpen"); if (opener) { opener.addEventListener("click", function (e) { e.preventDefault(); showBanner(); }); } } /* ---- the trial conversion: the page hands us its real submit function ---- * * Called by /static/black-friday.js when #trialForm is submitted. With consent we fire the Ads * conversion and run the page's submit from the callback; a blocked/slow tag is covered by the * hard timeout, so the trial always completes. Without consent (or without the tag) we run the * submit immediately and fire nothing — the trial is never gated on the tag. */ window.o4pAds = window.o4pAds || {}; window.o4pAds.trialConversion = function (action) { if (typeof action !== "function") return; if (!cfg || !cfg.trialSendTo || !isGranted() || typeof window.gtag !== "function") { action(); return; } fireConversion(cfg.trialSendTo, action); }; /* ---- the booking conversion: fired on a click of the booking link ---- * * The booking happens on Google's domain, off-site, so a click is all we can observe. The * caller does NOT preventDefault: the link opens in a new tab on its own, so even a blocked, * slow or absent tag can never stop the booking page from opening — this only ADDS the event. * Without consent (or the label) it fires nothing and simply runs the (optional) action. */ window.o4pAds.bookConversion = function (action) { if (typeof action !== "function") action = function () {}; if (!cfg || !cfg.bookSendTo || !isGranted() || typeof window.gtag !== "function") { action(); return; } fireConversion(cfg.bookSendTo, action); }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();