Skip to content

[SURF-1901] feat(consent): relay host consent to Surface form iframes - #73

Open
paragmore wants to merge 2 commits into
mainfrom
feat/host-consent-relay
Open

[SURF-1901] feat(consent): relay host consent to Surface form iframes#73
paragmore wants to merge 2 commits into
mainfrom
feat/host-consent-relay

Conversation

@paragmore

@paragmore paragmore commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

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:

    // from your consent banner, once the visitor answers
    window.SurfaceSetConsent({ adTracking: true, surfaceAnalytics: true });

    Omitted or non-boolean categories count as not granted. A later call can withdraw.

  • Relaystore.sendConsentToIframes() posts { type: "surface:consent", sender: "surface_tag", consent } to every Surface iframe, through the same origin allowlist notifyIframe already used (extracted as postToSurfaceIframe, no behavior change). It is also re-sent on each SEND_DATA handshake, so a form that mounts after the banner was answered still learns about it.

  • src/index.ts — exposes SurfaceSetConsent and wires the change callback. A STORE_UPDATE push 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 through SurfaceSetConsent and the tag relays; ?mode=iframe loads no tag 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 — that is what lets the unmerged #5631 build be tested.
    • Accept all / Reject all / per-category choices, plus Change preferences to withdraw. Every delivery is written to the shared events log, and the page carries the DevTools network filter to watch.
  • Docs: CLAUDE.md gains a Consent section and the protocol list now names surface:consent and surface: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. The notifyIframe refactor 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). New consent.test.ts covers normalisation, partial and non-boolean answers, withdrawal, and the change callback; store.test.ts gains a case asserting consent reaches Surface iframes only, and only after the page has answered; message-listener.test.ts gains one asserting the handshake re-sends it.
  • pnpm run typecheck: clean.
  • pnpm run build re-run; surface_tag.js / surface_embed_v1.js regenerated.
  • Real-browser run of test/consent.html (Playwright, built bundle, form on dev.withsurface.com):
    • tag mode, Accept all{type:"surface:consent",sender:"surface_tag",consent:{adTracking:true,surfaceAnalytics:true}} relayed to https://dev.withsurface.com, followed by STORE_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 post surface:consent directly to the frame with the form's exact origin as targetOrigin;
    • across every run, zero messages reached a non-Surface iframe on the page.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Cp98sntpeRzPJx9a6Fj3RX

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.
@greptile-apps

greptile-apps Bot commented Aug 25, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds a host-page consent API and relays normalized consent state to Surface form iframes.

  • Exposes window.SurfaceSetConsent and stores the latest consent answer.
  • Sends consent when the answer changes and during iframe SEND_DATA handshakes.
  • Reuses the configured Surface-origin allowlist and updates generated bundles, tests, and documentation.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains within the scope of this follow-up review.

No blocking failure remains.

Reviews (2): Last reviewed commit: "test(consent): add an end-to-end consent..." | Re-trigger Greptile

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.
@paragmore paragmore changed the title feat(consent): relay host consent to Surface form iframes [SURF-1901] feat(consent): relay host consent to Surface form iframes Aug 25, 2026
@notion-workspace

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants