From a7b38339a6d41901149ed98c401a5f9771e04a20 Mon Sep 17 00:00:00 2001 From: Deeman Date: Fri, 20 Feb 2026 01:48:08 +0100 Subject: [PATCH] feat: cookie consent banner, defer Paddle.js to checkout pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add cookie consent banner (_cookie_banner.html) — fixed bottom bar with "Accept all" and "Manage preferences" (toggles for Essential/Functional); consent stored in cookie_consent cookie (1 year); no-JS = only essential cookies set (privacy-safe default) - Add "Manage Cookies" link to footer Legal section to re-open the banner - Extract Paddle.js init into _paddle.html partial; add {% block paddle %} to base.html (empty by default); override on export, supplier signup, and supplier dashboard pages — Paddle.js no longer loads on every page visit - Gate ab_test() on functional cookie consent: variant picked per-request always, but ab_* cookie only persisted when visitor has consented - Update privacy policy section 6: full cookie disclosure (essential, functional, payment categories + Umami cookieless note); fix "Plausible" → "Umami" in service providers list Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 8 ++ padelnomics/src/padelnomics/core.py | 19 +++- .../padelnomics/planner/templates/export.html | 1 + .../padelnomics/public/templates/privacy.html | 24 ++++- .../templates/suppliers/dashboard.html | 1 + .../suppliers/templates/suppliers/signup.html | 1 + .../padelnomics/templates/_cookie_banner.html | 96 +++++++++++++++++++ .../src/padelnomics/templates/_paddle.html | 26 +++++ .../src/padelnomics/templates/base.html | 31 +----- 9 files changed, 175 insertions(+), 32 deletions(-) create mode 100644 padelnomics/src/padelnomics/templates/_cookie_banner.html create mode 100644 padelnomics/src/padelnomics/templates/_paddle.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ab1f25..0b1c3aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added +- Cookie consent banner: fixed bottom bar with "Accept all" and "Manage preferences" (toggle for functional/A/B cookies); consent stored in `cookie_consent` cookie for 1 year; "Manage Cookies" link added to footer Legal section + +### Changed +- Defer Paddle.js loading to only the 3 pages that use checkout (export, supplier signup, supplier dashboard) — removed from global `base.html` head; all other pages no longer receive Paddle's third-party cookies +- Gate A/B test cookie (`ab_*`) on functional cookie consent: variant is still picked per-request for rendering, but the cookie is only persisted when the visitor has accepted functional cookies +- Privacy policy section 6 (Cookies): full disclosure of all cookie categories (essential, functional, payment); fix "Plausible" → "Umami" in service providers list + ### Changed - Auto-create Resend audiences per blueprint: `capture_waitlist_email()` now derives the audience name from `request.blueprints[0]` (e.g., `waitlist-auth`, `waitlist-suppliers`) and lazily creates audiences via the Resend API on first use, caching IDs in a new `resend_audiences` table; removes `RESEND_AUDIENCE_WAITLIST` env var — only `RESEND_API_KEY` needed diff --git a/padelnomics/src/padelnomics/core.py b/padelnomics/src/padelnomics/core.py index 30e46a6..7dcb14d 100644 --- a/padelnomics/src/padelnomics/core.py +++ b/padelnomics/src/padelnomics/core.py @@ -464,13 +464,25 @@ def slugify(text: str, max_length_chars: int = 80) -> str: # A/B Testing # ============================================================================= +def _has_functional_consent() -> bool: + """Return True if the visitor has accepted functional cookies.""" + return "functional" in request.cookies.get("cookie_consent", "") + + def ab_test(experiment: str, variants: tuple = ("control", "treatment")): - """Assign visitor to an A/B test variant via cookie, tag Umami pageviews.""" + """Assign visitor to an A/B test variant, tag Umami pageviews. + + Only persists the variant cookie when the visitor has given functional + cookie consent. Without consent a random variant is picked per-request + (so the page renders fine and Umami is tagged), but no cookie is set. + """ def decorator(f): @wraps(f) async def wrapper(*args, **kwargs): cookie_key = f"ab_{experiment}" - assigned = request.cookies.get(cookie_key) + has_consent = _has_functional_consent() + + assigned = request.cookies.get(cookie_key) if has_consent else None if assigned not in variants: assigned = random.choice(variants) @@ -478,7 +490,8 @@ def ab_test(experiment: str, variants: tuple = ("control", "treatment")): g.ab_tag = f"{experiment}-{assigned}" response = await make_response(await f(*args, **kwargs)) - response.set_cookie(cookie_key, assigned, max_age=30 * 24 * 60 * 60) + if has_consent: + response.set_cookie(cookie_key, assigned, max_age=30 * 24 * 60 * 60) return response return wrapper return decorator diff --git a/padelnomics/src/padelnomics/planner/templates/export.html b/padelnomics/src/padelnomics/planner/templates/export.html index 7212f0c..37b3119 100644 --- a/padelnomics/src/padelnomics/planner/templates/export.html +++ b/padelnomics/src/padelnomics/planner/templates/export.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% block title %}Export Business Plan - {{ config.APP_NAME }}{% endblock %} +{% block paddle %}{% include "_paddle.html" %}{% endblock %} {% block head %}