Public Access
feat(bf): gate the Google Ads tag behind a consent banner; fire the buy-click conversion proxy
/odoo-black-friday/ only, page-side. No Odoo change, no ad-account change.
- static/ads-consent.js (new): default-DENY consent; nothing loads and no request
fires until Accept. On Decline (or switching to Decline later) the _gcl_au/_gcl_aw
cookies are cleared. A footer "Cookie settings" link reopens the banner.
- conversion: on submit of either /shop/post_basket buy form (offer block + On the
Rise card), gtag('event','conversion',{send_to: AW-18065522009/vnDUCPDxt7AdENn6p6ZD})
then form.submit() from event_callback, with a 700 ms hard-timeout fallback so a
blocked tag can never swallow the buy. No consent / no tag -> native POST untouched.
- privacy.html: new "Advertising cookies" disclosure (what, why, how to decline).
- Plausible Buy marks and the checkout-side Purchase event untouched.
This commit is contained in:
1 parent
f119f041f0
commit
ae97f4f5eb
3 files changed
+310
-2
No files matched your search
@@ -382,6 +382,110 @@
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
/* ---- Google Ads consent banner (page-scoped; additive). Default DENY: the tag does
|
||||
not load until the visitor accepts. Fallback colour values guard the first paint,
|
||||
before /style.css (loaded async) defines the tokens. ---- */
|
||||
.ads-consent {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
background: var(--midnight, #0d1b2e);
|
||||
color: var(--cream, #f4f0e8);
|
||||
border-top: 2px solid var(--brass, #c9a227);
|
||||
box-shadow: 0 -8px 30px rgba(13, 27, 46, 0.28);
|
||||
}
|
||||
.ads-consent[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.ads-consent-inner {
|
||||
width: 86%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ads-consent-title {
|
||||
font-family: "Marcellus", Georgia, serif;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--brass-bright);
|
||||
margin: 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.ads-consent-text {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
color: rgba(244, 240, 232, 0.9);
|
||||
margin: 0;
|
||||
flex: 1 1 340px;
|
||||
}
|
||||
.ads-consent-text a {
|
||||
color: var(--brass-bright);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.ads-consent-actions {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
.ads-consent-btn {
|
||||
font-family: "Marcellus", Georgia, serif;
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.04em;
|
||||
border-radius: var(--radius-pill, 2rem);
|
||||
padding: 0.55rem 1.4rem;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--brass, #c9a227);
|
||||
transition: background 0.2s, color 0.2s, border-color 0.2s;
|
||||
}
|
||||
.ads-consent-btn--accept {
|
||||
background: var(--brass, #c9a227);
|
||||
color: var(--midnight, #0d1b2e);
|
||||
}
|
||||
.ads-consent-btn--accept:hover,
|
||||
.ads-consent-btn--accept:focus-visible {
|
||||
background: var(--brass-bright);
|
||||
border-color: var(--brass-bright);
|
||||
}
|
||||
.ads-consent-btn--decline {
|
||||
background: transparent;
|
||||
color: var(--cream, #f4f0e8);
|
||||
border-color: rgba(244, 240, 232, 0.45);
|
||||
}
|
||||
.ads-consent-btn--decline:hover,
|
||||
.ads-consent-btn--decline:focus-visible {
|
||||
border-color: var(--cream, #f4f0e8);
|
||||
color: #fff;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.ads-consent-inner {
|
||||
padding: 0.85rem 0;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.ads-consent-actions {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.ads-consent-btn {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
/* The "change your choice later" link in the footer: brass for AA contrast on midnight
|
||||
(the generic .fcopy a is rgba(255,255,255,.4) = 3.8:1). */
|
||||
.fcopy a#adsConsentOpen {
|
||||
color: var(--brass, #c9a227);
|
||||
}
|
||||
.fcopy a#adsConsentOpen:hover {
|
||||
color: var(--brass-bright);
|
||||
}
|
||||
</style>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
@@ -953,11 +1057,42 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fcopy">Copyright © ODOO4projects LLC</div>
|
||||
<div class="fcopy">Copyright © ODOO4projects LLC · <a href="#" id="adsConsentOpen">Cookie settings</a></div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Google Ads consent banner — default DENY. The tag loads only after "Accept".
|
||||
Markup lives in the page; JS (static/ads-consent.js) reveals it and drives it.
|
||||
With JavaScript off it stays hidden and the native buy forms submit untouched.
|
||||
See wiki/concepts/conversion-tracking-contract. -->
|
||||
<div
|
||||
id="adsConsent"
|
||||
class="ads-consent"
|
||||
role="region"
|
||||
aria-label="Advertising cookie consent"
|
||||
hidden
|
||||
>
|
||||
<div class="ads-consent-inner">
|
||||
<p class="ads-consent-title">Advertising cookies</p>
|
||||
<p class="ads-consent-text">
|
||||
We would like to measure our Black Friday ads with Google. That uses optional
|
||||
advertising cookies. Accept or decline: the page works the same either way, and you
|
||||
can change your choice later from the “Cookie settings” link in the footer.
|
||||
See our <a href="/privacy.html">privacy policy</a>.
|
||||
</p>
|
||||
<div class="ads-consent-actions">
|
||||
<button type="button" id="adsConsentAccept" class="ads-consent-btn ads-consent-btn--accept">
|
||||
Accept
|
||||
</button>
|
||||
<button type="button" id="adsConsentDecline" class="ads-consent-btn ads-consent-btn--decline">
|
||||
Decline
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
||||
<script defer src="/static/black-friday.js"></script>
|
||||
<script defer src="/static/ads-consent.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+9
-1
@@ -121,7 +121,7 @@
|
||||
<div class="container">
|
||||
<h1>Privacy Policy</h1>
|
||||
<p style="margin-top: 0.5rem; color: rgba(33, 37, 41, 0.6); font-size: 0.85rem;">
|
||||
Last updated 2026-10-21
|
||||
Last updated 2026-11-02
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -173,6 +173,14 @@
|
||||
<p>Our pages load Stripe (payments), Google Fonts, Font Awesome, GSAP, and our own agent widget from our server. Like any website, these requests can see the IP address your browser connects from.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-card">
|
||||
<h2>Advertising cookies</h2>
|
||||
<p>On our Black Friday page we would like to measure our advertising with Google Ads. This is optional, and it is off until you turn it on.</p>
|
||||
<p>If you accept, the Google Ads tag loads and sets advertising cookies (named <strong>_gcl_au</strong> and <strong>_gcl_aw</strong>). They let Google tell us which ad or search led to a visit, so we spend our budget on the ads that work. They do not change what you pay, and we do not sell your data.</p>
|
||||
<p>If you decline, the tag does not load and those cookies are not set. The page works the same either way. You can change your choice at any time with the “Cookie settings” link in the footer of the Black Friday page, or by clearing cookies in your browser.</p>
|
||||
<p>Our privacy-friendly analytics are separate: they set no cookies and collect no personal data.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-card">
|
||||
<h2>Email</h2>
|
||||
<p>We email you about your account, your invoices and your service. Marketing email carries an unsubscribe link, and unsubscribing does not stop service email about an active account.</p>
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/* 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).
|
||||
*
|
||||
* 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 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.
|
||||
*
|
||||
* 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).
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var AW_ID = "AW-18065522009";
|
||||
var SEND_TO = "AW-18065522009/vnDUCPDxt7AdENn6p6ZD";
|
||||
var STORE_KEY = "o4p_ads_consent"; // "granted" | "denied"
|
||||
var SUBMIT_FALLBACK_MS = 700; // hard cap: submit even if the tag never answers
|
||||
|
||||
/* ---- 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 (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", AW_ID);
|
||||
var s = document.createElement("script");
|
||||
s.async = true;
|
||||
s.src = "https://www.googletagmanager.com/gtag/js?id=" + AW_ID;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
/* ---- the conversion proxy, fired on a buy-form submit ---- */
|
||||
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;
|
||||
|
||||
// No consent, or no tag: do nothing, let the native POST go through untouched.
|
||||
if (!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: SEND_TO,
|
||||
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;
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user