diff --git a/CLAUDE.md b/CLAUDE.md index cdc3614..85a63f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,15 +87,23 @@ Forms whose Privacy settings put a category on "On consent" load no scripts for it until the host page reports the visitor's answer: ```js -window.SurfaceSetConsent({ adTracking: true, surfaceAnalytics: true }); +window.SurfaceSetConsent({ adTracking: true, surfaceAnalytics: true, cookieTracking: true }); ``` `consent.ts` holds the answer in module state and notifies `src/index.ts`, which relays `surface:consent` to every Surface iframe (and re-sends it on each `SEND_DATA` handshake, for forms that mount after the banner was answered). -Omitted categories count as not granted. The categories mirror the form-render -gate in `surface_forms` (`lib/client/thirdParty/`) — keep the message shape in -sync with its `hostConsent.ts`. +Every call is a complete snapshot: omitted categories count as not granted. The +categories mirror the form-render gate in `surface_forms` +(`lib/client/thirdParty/`) — keep the message shape in sync with its +`hostConsent.ts`. + +`cookieTracking` also gates the tag's own host-side work, but only when the +` + +``` + +With the attribute, the tag does no visitor recognition, sets no journey cookies +and forwards no page cookies to Surface forms until `cookieTracking` is granted. +Form rendering and submission work regardless. Without the attribute the tag +behaves exactly as before. See `CLAUDE.md` for the message contract. diff --git a/src/consent/consent.test.ts b/src/consent/consent.test.ts index 579d449..acd4bb6 100644 --- a/src/consent/consent.test.ts +++ b/src/consent/consent.test.ts @@ -20,9 +20,16 @@ describe("surface consent", () => { expect(getSurfaceConsent()).toEqual({ adTracking: true, surfaceAnalytics: false, + cookieTracking: false, }); }); + it("treats each answer as a complete snapshot, so an older two-field call denies cookies", () => { + setSurfaceConsent({ adTracking: true, surfaceAnalytics: true, cookieTracking: true }); + setSurfaceConsent({ adTracking: true, surfaceAnalytics: true }); + expect(getSurfaceConsent()?.cookieTracking).toBe(false); + }); + it("ignores non-boolean values", () => { setSurfaceConsent({ adTracking: "yes" as unknown as boolean }); expect(getSurfaceConsent()?.adTracking).toBe(false); @@ -34,6 +41,7 @@ describe("surface consent", () => { expect(getSurfaceConsent()).toEqual({ adTracking: false, surfaceAnalytics: true, + cookieTracking: false, }); }); diff --git a/src/consent/consent.ts b/src/consent/consent.ts index 9c8f39c..2c537c8 100644 --- a/src/consent/consent.ts +++ b/src/consent/consent.ts @@ -3,13 +3,18 @@ export const SURFACE_CONSENT_MESSAGE_TYPE = "surface:consent"; /** - * Categories of third-party calls a Surface form can be told to wait for. They + * Categories of optional tracking a Surface form can be told to wait for. They * mirror the form's Privacy settings: a category set to "On consent" there stays * off until this page reports it as granted. + * + * `cookieTracking` also gates this tag's own host-side work — visitor + * recognition, the journey cookies and forwarding the page's cookies — when the + * script is loaded with `data-consent-mode`. */ export interface SurfaceConsent { adTracking: boolean; surfaceAnalytics: boolean; + cookieTracking: boolean; } let consent: SurfaceConsent | null = null; @@ -23,19 +28,22 @@ export const onSurfaceConsentChange = (callback: () => void): void => { }; /** - * Public API — call from a consent banner once the visitor answers: + * Public API — call from a consent banner once the visitor answers, and again + * whenever the answer changes: * * ```js - * window.SurfaceSetConsent({ adTracking: true, surfaceAnalytics: true }); + * window.SurfaceSetConsent({ adTracking: true, surfaceAnalytics: true, cookieTracking: true }); * ``` * - * Omitted categories count as not granted. Calling again with `false` stops - * further tracking, but cannot unload vendor scripts a form already started. + * Every call is a complete snapshot: omitted categories count as not granted. + * Calling again with `false` stops further tracking, but cannot unload vendor + * scripts a form already started. */ export const setSurfaceConsent = (granted: Partial): void => { consent = { adTracking: granted?.adTracking === true, surfaceAnalytics: granted?.surfaceAnalytics === true, + cookieTracking: granted?.cookieTracking === true, }; onChange?.(); }; diff --git a/src/index.ts b/src/index.ts index 521ecf2..0d1fca2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,8 +34,10 @@ w.SurfaceSetConsent = setSurfaceConsent; // Relay a consent answer to the forms on the page. The store push goes with it // so a form that was blocked until now still gets the parent URL params it -// needs to fire conversions in first-party context. +// needs to fire conversions in first-party context. Under data-consent-mode the +// tag's own recognition and journey work start or stop here too. onSurfaceConsentChange(() => { + SurfaceTagStore.applyConsent(); SurfaceTagStore.sendConsentToIframes(); SurfaceTagStore.sendPayloadToIframes("STORE_UPDATE"); }); diff --git a/src/lead/identify.ts b/src/lead/identify.ts index b2b4c28..eb1278e 100644 --- a/src/lead/identify.ts +++ b/src/lead/identify.ts @@ -29,6 +29,10 @@ export function setLeadDataWithTTL(data: Omit): void { localStorage.setItem("surfaceLeadData", JSON.stringify(item)); } +export function clearLeadData(): void { + localStorage.removeItem("surfaceLeadData"); +} + export function getLeadDataWithTTL(): LeadData | null { const itemStr = localStorage.getItem("surfaceLeadData"); if (!itemStr) return null; diff --git a/src/runtime-config.ts b/src/runtime-config.ts index 2c36beb..dacfc05 100644 --- a/src/runtime-config.ts +++ b/src/runtime-config.ts @@ -6,6 +6,10 @@ import { } from "./constants"; export const CUSTOM_DOMAIN_ATTRIBUTE = "data-custom-domain"; +// Present on the