diff --git a/odoo-black-friday/index.html b/odoo-black-friday/index.html
index f96dfc6..b9367f4 100644
--- a/odoo-black-friday/index.html
+++ b/odoo-black-friday/index.html
@@ -1063,7 +1063,9 @@
diff --git a/static/ads-consent.js b/static/ads-consent.js
index 0c97324..23d0a71 100644
--- a/static/ads-consent.js
+++ b/static/ads-consent.js
@@ -9,20 +9,24 @@
* _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 CONVERSION IS: a CLICK PROXY, not a sale. It fires when a buy form on this page
- * is submitted. The real purchase is measured on the checkout host (20.odoo4projects.com)
- * by odoo-dev. See wiki/concepts/conversion-tracking-contract. Never label it a sale.
+ * 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 a buy. On submit we fire the event and submit
- * the form from the event callback, with a hard timeout that submits anyway. With JavaScript
- * off the native POST forms submit untouched (no banner, no conversion, no error).
+ * 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 } — read from the banner, never hard-coded
+ var cfg = null; // { id, sendTo, trialSendTo } — from the banner, never hard-coded
/* ---- the visitor's stored choice ---- */
function readChoice() {
@@ -79,7 +83,28 @@
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;
@@ -141,7 +166,11 @@
var id = banner.getAttribute("data-ads-id");
var sendTo = banner.getAttribute("data-ads-send-to");
if (!id || !sendTo) return;
- cfg = { id: id, sendTo: sendTo };
+ cfg = {
+ id: id,
+ sendTo: sendTo,
+ trialSendTo: banner.getAttribute("data-ads-trial-send-to"),
+ };
var choice = readChoice();
if (choice === "granted") {
@@ -165,6 +194,22 @@
}
}
+ /* ---- 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 {
diff --git a/static/black-friday.js b/static/black-friday.js
index eb26760..50c887d 100644
--- a/static/black-friday.js
+++ b/static/black-friday.js
@@ -241,25 +241,37 @@
var location = trialLoc ? trialLoc.value : '';
if (!email) return;
var btn = trialForm.querySelector('.btn-brass');
- btn.textContent = 'Sending...';
- btn.disabled = true;
- fetch('https://n8n.odoo4projects.com/webhook/ODOO4projects/trial', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(UTM.inject({ email: email, location: location }))
- }).then(function () {
- if (window.plausible) window.plausible('Trial');
- trialForm.style.display = 'none';
- document.getElementById('trialSuccess').classList.add('visible');
- }).catch(function () {
- btn.textContent = 'Send';
- btn.disabled = false;
- var errorMsg = document.createElement('div');
- errorMsg.className = 'trial-error visible';
- errorMsg.textContent = 'Something went wrong. Please try again.';
- trialForm.appendChild(errorMsg);
- setTimeout(function () { errorMsg.classList.remove('visible'); }, 3000);
- });
+
+ function sendTrial() {
+ btn.textContent = 'Sending...';
+ btn.disabled = true;
+ fetch('https://n8n.odoo4projects.com/webhook/ODOO4projects/trial', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(UTM.inject({ email: email, location: location }))
+ }).then(function () {
+ if (window.plausible) window.plausible('Trial');
+ trialForm.style.display = 'none';
+ document.getElementById('trialSuccess').classList.add('visible');
+ }).catch(function () {
+ btn.textContent = 'Send';
+ btn.disabled = false;
+ var errorMsg = document.createElement('div');
+ errorMsg.className = 'trial-error visible';
+ errorMsg.textContent = 'Something went wrong. Please try again.';
+ trialForm.appendChild(errorMsg);
+ setTimeout(function () { errorMsg.classList.remove('visible'); }, 3000);
+ });
+ }
+
+ /* When the consent gate is present it fires the Google Ads trial conversion and runs
+ sendTrial from the tag's callback, with a hard timeout that runs it anyway — so a
+ blocked or slow tag can never swallow the trial. Without the gate, send directly. */
+ if (window.o4pAds && typeof window.o4pAds.trialConversion === 'function') {
+ window.o4pAds.trialConversion(sendTrial);
+ } else {
+ sendTrial();
+ }
});
}
})();