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