diff --git a/CLAUDE.md b/CLAUDE.md index 3adb5d1..cdc3614 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,8 +78,24 @@ No automated tests, linter, or CI pipeline. Testing is manual and browser-based. ### PostMessage Protocol -- **To iframe:** `STORE_UPDATE` (cookies, URL params, partial fill data), `LEAD_DATA_UPDATE` (leadId, sessionId, fingerprint) -- **From iframe:** `SEND_DATA` (iframe requests current store data) +- **To iframe:** `STORE_UPDATE` (cookies, URL params, partial fill data), `LEAD_DATA_UPDATE` (leadId, sessionId, fingerprint), `surface:consent` (which third-party categories the visitor consented to) +- **From iframe:** `SEND_DATA` (iframe requests current store data), `surface:conversion` (iframe asks the parent to fire an ad pixel first-party) + +### Consent (`src/consent/`) + +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 }); +``` + +`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`. ### Key APIs diff --git a/src/consent/consent.test.ts b/src/consent/consent.test.ts new file mode 100644 index 0000000..579d449 --- /dev/null +++ b/src/consent/consent.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { + getSurfaceConsent, + onSurfaceConsentChange, + setSurfaceConsent, +} from "./consent"; + +describe("surface consent", () => { + beforeEach(() => { + onSurfaceConsentChange(() => {}); + }); + + it("reports nothing granted until the page answers", () => { + // Module state, so this only holds before the first setSurfaceConsent call. + expect(getSurfaceConsent()).toBe(null); + }); + + it("normalises a partial answer — omitted categories are not granted", () => { + setSurfaceConsent({ adTracking: true }); + expect(getSurfaceConsent()).toEqual({ + adTracking: true, + surfaceAnalytics: false, + }); + }); + + it("ignores non-boolean values", () => { + setSurfaceConsent({ adTracking: "yes" as unknown as boolean }); + expect(getSurfaceConsent()?.adTracking).toBe(false); + }); + + it("lets a later answer withdraw consent", () => { + setSurfaceConsent({ adTracking: true, surfaceAnalytics: true }); + setSurfaceConsent({ adTracking: false, surfaceAnalytics: true }); + expect(getSurfaceConsent()).toEqual({ + adTracking: false, + surfaceAnalytics: true, + }); + }); + + it("notifies the relay on every answer", () => { + const onChange = vi.fn(); + onSurfaceConsentChange(onChange); + + setSurfaceConsent({ adTracking: true }); + setSurfaceConsent({ adTracking: false }); + + expect(onChange).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/consent/consent.ts b/src/consent/consent.ts new file mode 100644 index 0000000..9c8f39c --- /dev/null +++ b/src/consent/consent.ts @@ -0,0 +1,41 @@ +// Wire contract with the iframe (surface_forms form-render `hostConsent.ts`). +// Keep in sync. +export const SURFACE_CONSENT_MESSAGE_TYPE = "surface:consent"; + +/** + * Categories of third-party calls 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. + */ +export interface SurfaceConsent { + adTracking: boolean; + surfaceAnalytics: boolean; +} + +let consent: SurfaceConsent | null = null; +let onChange: (() => void) | null = null; + +/** Null until the page has answered — forms treat that as nothing granted. */ +export const getSurfaceConsent = (): SurfaceConsent | null => consent; + +export const onSurfaceConsentChange = (callback: () => void): void => { + onChange = callback; +}; + +/** + * Public API — call from a consent banner once the visitor answers: + * + * ```js + * window.SurfaceSetConsent({ adTracking: true, surfaceAnalytics: true }); + * ``` + * + * 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, + }; + onChange?.(); +}; diff --git a/src/index.ts b/src/index.ts index c330749..521ecf2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { setEnvironmentId, } from "./lead/identify"; import { SurfaceStore } from "./store/store"; +import { onSurfaceConsentChange, setSurfaceConsent } from "./consent/consent"; import { SurfaceExternalForm } from "./external-form/external-form"; import { SurfaceEmbed } from "./embed/embed"; import { resolveOpenTriggersOnLoad } from "./open-triggers/open-triggers"; @@ -29,6 +30,15 @@ w.SurfaceIdentifyLead = identifyLead; w.SurfaceSetLeadDataWithTTL = setLeadDataWithTTL; w.SurfaceGetLeadDataWithTTL = getLeadDataWithTTL; w.SurfaceGetSiteIdFromScript = getSiteIdFromScript; +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. +onSurfaceConsentChange(() => { + SurfaceTagStore.sendConsentToIframes(); + SurfaceTagStore.sendPayloadToIframes("STORE_UPDATE"); +}); // Auto-open a form when the host URL carries a configured `?=true` param. // Fire-and-forget; only touches the network when params are present. diff --git a/src/store/message-listener.test.ts b/src/store/message-listener.test.ts index 1060e3a..657387e 100644 --- a/src/store/message-listener.test.ts +++ b/src/store/message-listener.test.ts @@ -16,6 +16,7 @@ const FORMS_ORIGIN = "https://forms.withsurface.com"; const makeStore = () => ({ sendPayloadToIframes: vi.fn(), + sendConsentToIframes: vi.fn(), clearUserJourney: vi.fn(), log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, }) as unknown as SurfaceStore; @@ -48,6 +49,15 @@ describe("initializeMessageListener", () => { expect(store.sendPayloadToIframes).toHaveBeenCalledWith("STORE_UPDATE"); }); + it("re-sends consent on the handshake, so a late-mounting form learns the page's answer", () => { + const store = makeStore(); + initializeMessageListener(store); + + dispatch({ type: "SEND_DATA", sender: "surface_form" }); + + expect(store.sendConsentToIframes).toHaveBeenCalledTimes(1); + }); + it("with an environment id: pushes STORE_UPDATE, identifies, then pushes LEAD_DATA_UPDATE", async () => { vi.mocked(getEnvironmentId).mockReturnValue("env_123"); const store = makeStore(); diff --git a/src/store/message-listener.ts b/src/store/message-listener.ts index 560ca78..a28a51e 100644 --- a/src/store/message-listener.ts +++ b/src/store/message-listener.ts @@ -17,6 +17,8 @@ export function initializeMessageListener(store: SurfaceStore): void { if (event.data.type === "SEND_DATA") { store.sendPayloadToIframes("STORE_UPDATE"); + // A form that booted after the banner was answered learns consent here. + store.sendConsentToIframes(); const envId = getEnvironmentId(); if (envId) { diff --git a/src/store/store.test.ts b/src/store/store.test.ts index d5a7243..c53032a 100644 --- a/src/store/store.test.ts +++ b/src/store/store.test.ts @@ -4,6 +4,7 @@ import { identifyLead, getLeadDataWithTTL } from "../lead/identify"; import { initializeUserJourneyTracking, updateUserJourneyOnRouteChange } from "./user-journey"; import { onRouteChange } from "../utils/route-observer"; import type { LeadData } from "../types"; +import { setSurfaceConsent } from "../consent/consent"; vi.mock("./message-listener", () => ({ initializeMessageListener: vi.fn(), @@ -179,4 +180,33 @@ describe("SurfaceStore postMessage protocol", () => { ); expect(otherPost).not.toHaveBeenCalled(); }); + + it("relays consent only to Surface iframes, and only once the page has answered", () => { + const surfaceIframe = addIframe(SURFACE_IFRAME_SRC); + const otherIframe = addIframe("https://example.com/embed"); + const store = new SurfaceStore(null); + + const surfacePost = vi + .spyOn(surfaceIframe.contentWindow as Window, "postMessage") + .mockImplementation(() => {}); + const otherPost = vi + .spyOn(otherIframe.contentWindow as Window, "postMessage") + .mockImplementation(() => {}); + + store.sendConsentToIframes(); + expect(surfacePost).not.toHaveBeenCalled(); + + setSurfaceConsent({ adTracking: true }); + store.sendConsentToIframes(); + + expect(surfacePost).toHaveBeenCalledWith( + { + type: "surface:consent", + sender: "surface_tag", + consent: { adTracking: true, surfaceAnalytics: false }, + }, + "https://forms.withsurface.com" + ); + expect(otherPost).not.toHaveBeenCalled(); + }); }); diff --git a/src/store/store.ts b/src/store/store.ts index c8f7c9f..eb9a3dd 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -1,4 +1,8 @@ import { VALID_EMBED_TYPES } from "../constants"; +import { + getSurfaceConsent, + SURFACE_CONSENT_MESSAGE_TYPE, +} from "../consent/consent"; import { isDebugMode } from "../utils/debug"; import { createLogger } from "../utils/logger"; import { parseCookies } from "../utils/cookies"; @@ -163,19 +167,40 @@ export class SurfaceStore { const target = iframe || document.querySelector("#surface-iframe"); if (!target) return; + this.postToSurfaceIframe(target, { + type, + payload: this.getPayload(), + sender: "surface_tag", + }); + } + + private postToSurfaceIframe(target: HTMLIFrameElement, message: unknown): void { try { const targetOrigin = new URL(target.src).origin; if (!this.surfaceDomains.includes(targetOrigin)) return; - target.contentWindow?.postMessage( - { type, payload: this.getPayload(), sender: "surface_tag" }, - targetOrigin - ); + target.contentWindow?.postMessage(message, targetOrigin); } catch { // Ignore invalid iframe URLs. } } + // Relays the page's consent answer to every Surface form on it. Forms with a + // category set to "On consent" stay dark until this arrives, so it is also + // re-sent on each SEND_DATA handshake for frames that mount later. + sendConsentToIframes(): void { + const consent = getSurfaceConsent(); + if (!consent) return; + + document.querySelectorAll("iframe").forEach((iframe) => + this.postToSurfaceIframe(iframe, { + type: SURFACE_CONSENT_MESSAGE_TYPE, + sender: "surface_tag", + consent, + }) + ); + } + getUrlParams(): Record { return getUrlParams(); } diff --git a/surface_embed_v1.js b/surface_embed_v1.js index 076e3ac..9d14ceb 100644 --- a/surface_embed_v1.js +++ b/surface_embed_v1.js @@ -216,6 +216,22 @@ return null; } + // src/consent/consent.ts + var SURFACE_CONSENT_MESSAGE_TYPE = "surface:consent"; + var consent = null; + var onChange = null; + var getSurfaceConsent = () => consent; + var onSurfaceConsentChange = (callback) => { + onChange = callback; + }; + var setSurfaceConsent = (granted) => { + consent = { + adTracking: granted?.adTracking === true, + surfaceAnalytics: granted?.surfaceAnalytics === true + }; + onChange?.(); + }; + // src/utils/debug.ts var cached = null; function isDebugMode() { @@ -496,6 +512,7 @@ } if (event.data.type === "SEND_DATA") { store.sendPayloadToIframes("STORE_UPDATE"); + store.sendConsentToIframes(); const envId = getEnvironmentId(); if (envId) { const identify = store.config?.customOrigin ? identifyLead(envId, store.config) : identifyLead(envId); @@ -756,16 +773,34 @@ notifyIframe(iframe, type) { const target = iframe || document.querySelector("#surface-iframe"); if (!target) return; + this.postToSurfaceIframe(target, { + type, + payload: this.getPayload(), + sender: "surface_tag" + }); + } + postToSurfaceIframe(target, message) { try { const targetOrigin = new URL(target.src).origin; if (!this.surfaceDomains.includes(targetOrigin)) return; - target.contentWindow?.postMessage( - { type, payload: this.getPayload(), sender: "surface_tag" }, - targetOrigin - ); + target.contentWindow?.postMessage(message, targetOrigin); } catch { } } + // Relays the page's consent answer to every Surface form on it. Forms with a + // category set to "On consent" stay dark until this arrives, so it is also + // re-sent on each SEND_DATA handshake for frames that mount later. + sendConsentToIframes() { + const consent2 = getSurfaceConsent(); + if (!consent2) return; + document.querySelectorAll("iframe").forEach( + (iframe) => this.postToSurfaceIframe(iframe, { + type: SURFACE_CONSENT_MESSAGE_TYPE, + sender: "surface_tag", + consent: consent2 + }) + ); + } getUrlParams() { return getUrlParams(); } @@ -2473,6 +2508,11 @@ w2.SurfaceSetLeadDataWithTTL = setLeadDataWithTTL; w2.SurfaceGetLeadDataWithTTL = getLeadDataWithTTL; w2.SurfaceGetSiteIdFromScript = getSiteIdFromScript; + w2.SurfaceSetConsent = setSurfaceConsent; + onSurfaceConsentChange(() => { + SurfaceTagStore.sendConsentToIframes(); + SurfaceTagStore.sendPayloadToIframes("STORE_UPDATE"); + }); void resolveOpenTriggersOnLoad(environmentId2, runtimeConfig2); initReview(); })(); diff --git a/surface_tag.js b/surface_tag.js index 076e3ac..9d14ceb 100644 --- a/surface_tag.js +++ b/surface_tag.js @@ -216,6 +216,22 @@ return null; } + // src/consent/consent.ts + var SURFACE_CONSENT_MESSAGE_TYPE = "surface:consent"; + var consent = null; + var onChange = null; + var getSurfaceConsent = () => consent; + var onSurfaceConsentChange = (callback) => { + onChange = callback; + }; + var setSurfaceConsent = (granted) => { + consent = { + adTracking: granted?.adTracking === true, + surfaceAnalytics: granted?.surfaceAnalytics === true + }; + onChange?.(); + }; + // src/utils/debug.ts var cached = null; function isDebugMode() { @@ -496,6 +512,7 @@ } if (event.data.type === "SEND_DATA") { store.sendPayloadToIframes("STORE_UPDATE"); + store.sendConsentToIframes(); const envId = getEnvironmentId(); if (envId) { const identify = store.config?.customOrigin ? identifyLead(envId, store.config) : identifyLead(envId); @@ -756,16 +773,34 @@ notifyIframe(iframe, type) { const target = iframe || document.querySelector("#surface-iframe"); if (!target) return; + this.postToSurfaceIframe(target, { + type, + payload: this.getPayload(), + sender: "surface_tag" + }); + } + postToSurfaceIframe(target, message) { try { const targetOrigin = new URL(target.src).origin; if (!this.surfaceDomains.includes(targetOrigin)) return; - target.contentWindow?.postMessage( - { type, payload: this.getPayload(), sender: "surface_tag" }, - targetOrigin - ); + target.contentWindow?.postMessage(message, targetOrigin); } catch { } } + // Relays the page's consent answer to every Surface form on it. Forms with a + // category set to "On consent" stay dark until this arrives, so it is also + // re-sent on each SEND_DATA handshake for frames that mount later. + sendConsentToIframes() { + const consent2 = getSurfaceConsent(); + if (!consent2) return; + document.querySelectorAll("iframe").forEach( + (iframe) => this.postToSurfaceIframe(iframe, { + type: SURFACE_CONSENT_MESSAGE_TYPE, + sender: "surface_tag", + consent: consent2 + }) + ); + } getUrlParams() { return getUrlParams(); } @@ -2473,6 +2508,11 @@ w2.SurfaceSetLeadDataWithTTL = setLeadDataWithTTL; w2.SurfaceGetLeadDataWithTTL = getLeadDataWithTTL; w2.SurfaceGetSiteIdFromScript = getSiteIdFromScript; + w2.SurfaceSetConsent = setSurfaceConsent; + onSurfaceConsentChange(() => { + SurfaceTagStore.sendConsentToIframes(); + SurfaceTagStore.sendPayloadToIframes("STORE_UPDATE"); + }); void resolveOpenTriggersOnLoad(environmentId2, runtimeConfig2); initReview(); })(); diff --git a/test/consent.css b/test/consent.css new file mode 100644 index 0000000..106a901 --- /dev/null +++ b/test/consent.css @@ -0,0 +1,117 @@ +/* Consent-mode test page: the stand-in cookie banner and its surroundings. */ + +/* The banner rows below set `display`, which would otherwise win over [hidden]. */ +.cookie-prefs[hidden], +.consent-status[hidden] { + display: none; +} + +.steps { + margin: 0; + padding-left: 20px; + line-height: 1.7; +} + +.steps li + li { + margin-top: 8px; +} + +.note { + color: #6b7280; + font-size: 13px; + line-height: 1.6; +} + +.filter-row { + display: flex; + align-items: center; + gap: 8px; + margin-top: 6px; +} + +.filter-row code { + background: #f5f5f5; + border-radius: 4px; + flex: 1; + overflow-x: auto; + padding: 6px 8px; + white-space: nowrap; +} + +.consent-iframe { + border: 1px solid #e0e0e0; + border-radius: 6px; + height: 600px; + width: 100%; +} + +/* Leave room for the fixed banner so it never covers the form. */ +body { + padding-bottom: 180px; +} + +.cookie-banner, +.consent-status { + background: #ffffff; + border-top: 1px solid #e0e0e0; + bottom: 0; + box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.08); + left: 0; + position: fixed; + right: 0; + z-index: 1000; +} + +.cookie-banner-body { + align-items: center; + display: flex; + gap: 24px; + justify-content: space-between; + margin: 0 auto; + max-width: 1000px; + padding: 20px; +} + +.cookie-copy p { + color: #4b5563; + font-size: 14px; + line-height: 1.6; + margin: 4px 0 0; +} + +.cookie-actions { + display: flex; + flex-shrink: 0; + gap: 8px; +} + +.cookie-prefs { + align-items: center; + border-top: 1px solid #f0f0f0; + display: flex; + flex-wrap: wrap; + gap: 20px; + margin: 0 auto; + max-width: 1000px; + padding: 14px 20px; +} + +.cookie-prefs label { + align-items: center; + display: flex; + font-size: 14px; + gap: 8px; +} + +.consent-status { + align-items: center; + display: flex; + gap: 16px; + justify-content: center; + padding: 14px 20px; +} + +.consent-status span { + font-size: 14px; + font-weight: 600; +} diff --git a/test/consent.html b/test/consent.html new file mode 100644 index 0000000..e579711 --- /dev/null +++ b/test/consent.html @@ -0,0 +1,174 @@ + + + + + + + Consent Mode Test - Surface Form + + + + + +
+

Consent Mode Test

+

← Back to Test Suite

+ +
+ Configuration:
+ +
+ + + +
+ Consent delivery: + Surface tag · + Direct iframe + — currently
+ Environment ID:
+ Custom domain declared to the tag:
+ Surface Tag:
+ Debug Mode: false (add ?surfaceDebug=true to enable) +
+ +
+

How to run this test

+
    +
  1. + In the form's Settings → Privacy tab, set Ad & conversion tracking + (and/or Surface analytics) to On consent. +
  2. +
  3. + Give the form something to block: a GTM container, GA4 measurement ID, Meta Pixel, or HubSpot + tracking ID on the other settings tabs. Surface analytics needs nothing configured. +
  4. +
  5. + Point Form URL above at that form and reload. Open DevTools → Network and filter on: +
    + googletagmanager|google-analytics|connect.facebook|facebook.com/tr|hs-scripts|posthog|vercel-scripts + +
    +
  6. +
  7. + Before answering the banner: no requests match. That is the whole point — + the form rendered, but nothing third-party loaded. +
  8. +
  9. + Accept in the banner: the matching vendor requests appear within a second, with no + page reload. Reject instead and the filter stays empty. +
  10. +
+

+ Withdrawing consent after accepting stops further calls but cannot unload scripts that already + started — reload the page to get back to a clean slate. +

+
+ +

Events Summary

+
+
0
Total Events
+
0
Sent to Iframe
+
0
Received from Iframe
+
0
STORE_UPDATE
+
0
LEAD_DATA_UPDATE
+
0
SEND_DATA
+
+ +

Events Log

+
+ + +
+
+ +
+

Embedded Form

+

+ A plain iframe, as a customer would write it. In tag mode the tag finds it on the page and relays + consent to it; in direct-iframe mode this page posts to it directly. +

+ +
+
+ + + + + + + + + + diff --git a/test/consent.js b/test/consent.js new file mode 100644 index 0000000..474b329 --- /dev/null +++ b/test/consent.js @@ -0,0 +1,117 @@ +// Drives the consent-mode test page: renders the config panel, wires the +// stand-in cookie banner, and delivers the answer the way the chosen embed +// shape would — window.SurfaceSetConsent (tag relays it) or a postMessage +// straight to the form frame (no tag on the page). + +const config = window.consentTestConfig; +const iframe = document.getElementById("consentIframe"); +const banner = document.getElementById("cookieBanner"); +const prefs = document.getElementById("cookiePrefs"); +const status = document.getElementById("consentStatus"); +const statusText = document.getElementById("consentStatusText"); +const adTrackingPref = document.getElementById("prefAdTracking"); +const surfaceAnalyticsPref = document.getElementById("prefSurfaceAnalytics"); + +const text = (id, value) => { + document.getElementById(id).textContent = value; +}; + +const describe = (consent) => + Object.entries(consent) + .map(([category, granted]) => `${category}: ${granted ? "granted" : "denied"}`) + .join(", "); + +function renderConfig() { + document.getElementById("formUrlInput").value = config.formSrc; + text("modeLabel", config.mode === "tag" ? "Surface tag" : "Direct iframe (no tag)"); + text("siteIdLabel", config.siteId); + text("customDomainLabel", config.customDomain || "none (production origin)"); + text("tagStatus", config.tagStatus); + text("debugMode", String(window.location.search.includes("surfaceDebug=true"))); + + if (!config.formOrigin) { + text("tagStatus", "form URL is not a valid absolute URL"); + return; + } + iframe.src = config.formSrc; +} + +function reloadWithFormUrl() { + const params = new URLSearchParams(window.location.search); + params.set("formSrc", document.getElementById("formUrlInput").value.trim()); + window.location.search = params.toString(); +} + +// The one call a real cookie banner would make. +function deliverConsent(consent) { + if (config.mode === "tag") { + if (typeof window.SurfaceSetConsent !== "function") { + logEvent( + { type: "CONSENT_NOT_DELIVERED", sender: "consent_banner", payload: { reason: "surface_tag.js did not load — run `pnpm run build` and serve from the repo root" } }, + "sent" + ); + return; + } + window.SurfaceSetConsent(consent); + logEvent({ type: "SurfaceSetConsent", sender: "consent_banner", payload: consent }, "sent"); + return; + } + + iframe.contentWindow.postMessage({ type: "surface:consent", consent }, config.formOrigin); + logEvent( + { type: "surface:consent", sender: "consent_banner", payload: { consent, targetOrigin: config.formOrigin } }, + "sent" + ); +} + +function answer(consent) { + deliverConsent(consent); + adTrackingPref.checked = consent.adTracking; + surfaceAnalyticsPref.checked = consent.surfaceAnalytics; + banner.hidden = true; + status.hidden = false; + statusText.textContent = `Consent — ${describe(consent)}`; +} + +function showBanner() { + status.hidden = true; + banner.hidden = false; + prefs.hidden = false; +} + +document.getElementById("reloadWithForm").addEventListener("click", reloadWithFormUrl); +document.getElementById("managePrefs").addEventListener("click", () => { + prefs.hidden = !prefs.hidden; +}); +document.getElementById("acceptAll").addEventListener("click", () => + answer({ adTracking: true, surfaceAnalytics: true }) +); +document.getElementById("rejectAll").addEventListener("click", () => + answer({ adTracking: false, surfaceAnalytics: false }) +); +document.getElementById("savePrefs").addEventListener("click", () => + answer({ adTracking: adTrackingPref.checked, surfaceAnalytics: surfaceAnalyticsPref.checked }) +); +document.getElementById("changePrefs").addEventListener("click", showBanner); +document.getElementById("copyFilter").addEventListener("click", (event) => { + navigator.clipboard.writeText(document.getElementById("vendorFilter").textContent.trim()); + event.target.textContent = "Copied"; + setTimeout(() => (event.target.textContent = "Copy"), 1500); +}); + +// event-monitor.js only logs messages from production Surface origins, so log +// the form frame's own traffic here — it may be a preview deploy or localhost. +window.addEventListener( + "message", + (event) => { + if (event.origin !== config.formOrigin || !event.data) return; + logEvent( + { type: event.data.type || "UNKNOWN", payload: event.data.payload || event.data, sender: event.data.sender || "iframe" }, + "received" + ); + }, + true +); + +initializeEventMonitoring(); +renderConfig(); diff --git a/test/index.html b/test/index.html index 80726b7..1a64fca 100644 --- a/test/index.html +++ b/test/index.html @@ -45,6 +45,12 @@

4. Input Trigger Embed

Test Input Trigger → + +