[SURF-1901] feat(consent): relay host consent to Surface form iframes - #73
Open
paragmore wants to merge 2 commits into
Open
[SURF-1901] feat(consent): relay host consent to Surface form iframes#73paragmore wants to merge 2 commits into
paragmore wants to merge 2 commits into
Conversation
Forms whose Privacy settings put a category on "On consent" load no vendor
scripts until the embedding page reports the visitor's answer. This adds the
page-side half of that handshake.
- `window.SurfaceSetConsent({ adTracking, surfaceAnalytics })` — call it from
a consent banner. Omitted categories count as not granted; a later call can
withdraw.
- The answer is relayed as `surface:consent` to every Surface iframe (origin
allowlist unchanged) and re-sent on each SEND_DATA handshake, so a form that
mounts after the banner was answered still learns about it.
- A store push rides along, so a form unblocked mid-session still gets the
parent URL params it needs to fire conversions in first-party context.
|
A stand-in cookie banner over a real embedded form, so the whole path can be exercised in a browser: form renders dark, banner answered, vendor scripts start with no reload. - `?mode=tag` (default) delivers the answer through window.SurfaceSetConsent and the tag relays it; `?mode=iframe` loads no tag at all and posts `surface:consent` straight to the frame, the way a customer embedding a plain <iframe> would. - `?formSrc=` points at any form; a non-production origin (preview deploy, custom domain) is declared to the tag via data-custom-domain automatically, which is what lets an unmerged build be tested. - Accept all / Reject all / per-category choices, plus Change preferences to withdraw; every delivery is written to the shared events log.
arjunsahai
approved these changes
Aug 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Page-side half of per-form consent mode. Pairs with trysurface/surface_forms#5631 (stacked on #5625 → #5624) — that PR's "On consent" setting does nothing for tag embeds without this one.
What changed
A form whose Privacy settings put a category on "On consent" loads no vendor scripts until the embedding page reports the visitor's answer. This adds the API that reports it, plus a test page that exercises the whole path.
src/consent/consent.ts(new) — module state plus the public API:Omitted or non-boolean categories count as not granted. A later call can withdraw.
Relay —
store.sendConsentToIframes()posts{ type: "surface:consent", sender: "surface_tag", consent }to every Surface iframe, through the same origin allowlistnotifyIframealready used (extracted aspostToSurfaceIframe, no behavior change). It is also re-sent on eachSEND_DATAhandshake, so a form that mounts after the banner was answered still learns about it.src/index.ts— exposesSurfaceSetConsentand wires the change callback. ASTORE_UPDATEpush rides along with each relay: a form unblocked mid-session still needs the parent URL params to fire conversions in first-party context.test/consent.html(new, linked from the test suite index) — a stand-in cookie banner over a real embedded form:?mode=tag(default) delivers throughSurfaceSetConsentand the tag relays;?mode=iframeloads no tag and postssurface:consentstraight to the frame, the way a customer embedding a plain<iframe>would.?formSrc=points at any form. A non-production origin (preview deploy, custom domain) is declared to the tag viadata-custom-domainautomatically — that is what lets the unmerged #5631 build be tested.Docs:
CLAUDE.mdgains a Consent section and the protocol list now namessurface:consentandsurface:conversion.Nothing changes for pages that never call the API — no consent is relayed until the first call, and the form side treats absence as not granted.
Why
Customers with a consent banner had to choose between loading tracking before consent or not at all. The form side can gate the scripts, but only the host page knows what the visitor answered, so it needs a way to say so. Plain
<iframe>embeds without the tag can post the same message themselves — the settings UI in #5631 hands them that snippet too.Risk / rollback
Low. No new listeners, no network calls, no behavior on pages that don't call
SurfaceSetConsent. ThenotifyIframerefactor is a pure extraction covered by the existing protocol test.test/is not part of the build. Revert to roll back.Validation
pnpm test: 3 files / 19 tests green (main: 2 files / 12). Newconsent.test.tscovers normalisation, partial and non-boolean answers, withdrawal, and the change callback;store.test.tsgains a case asserting consent reaches Surface iframes only, and only after the page has answered;message-listener.test.tsgains one asserting the handshake re-sends it.pnpm run typecheck: clean.pnpm run buildre-run;surface_tag.js/surface_embed_v1.jsregenerated.test/consent.html(Playwright, built bundle, form ondev.withsurface.com):{type:"surface:consent",sender:"surface_tag",consent:{adTracking:true,surfaceAnalytics:true}}relayed tohttps://dev.withsurface.com, followed bySTORE_UPDATE;?formSrc=pointed at the #5631 preview → the preview origin is auto-declared and joins the allowlist, and both messages target it;?mode=iframe→ no tag loads at all, and Reject all then a per-category Save choices postsurface:consentdirectly to the frame with the form's exact origin astargetOrigin;🤖 Generated with Claude Code
https://claude.ai/code/session_01Cp98sntpeRzPJx9a6Fj3RX