Files
ODOO4projects.com/static/ads-consent.js
T
webmaster 7497fbf588 feat(bf): fire the trial conversion behind the same consent gate
Adds the second Google Ads label to /odoo-black-friday/: the free-trial
form (#trialForm) now fires AW-18065522009/AtAPCIjjvrAdENn6p6ZD (action
7886320008 "Registrierung") when it is submitted.

Reuses the existing gate in static/ads-consent.js rather than a second
mechanism: the label is read from the page (data-ads-trial-send-to on
#adsConsent), so the script stays generic. The page hands the gate its
real submit function via window.o4pAds.trialConversion; the gate fires
the conversion and runs the submit from the event callback, with the same
700 ms hard timeout that runs it anyway. Nothing fires and no cookie is
set before consent (identical default-deny gate).

The buy conversion is untouched (comments only).
2026-11-02 09:23:14 +00:00

219 lines
7.6 KiB
JavaScript

/* 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"),
};
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);
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();