A monorepo containing three React SPAs that demonstrate how to implement and test the TrustArc GPC (Global Privacy Control) banner across cross-origin iframes using postMessage, plus how Content-Security-Policy affects embedding the TrustArc DSR form.
| Tool | Version |
|---|---|
| Node.js | v24.14.1 |
| pnpm | 10.13.1 |
| Package | |
|---|---|
| Framework | React 18 |
| Routing | React Router v6 |
| Build tool | Vite 5 |
| Package manager | pnpm (workspace monorepo) |
sample-react-app-gpc-banner-implementation/
├── package.json # Root — workspace scripts
├── pnpm-workspace.yaml # Declares spa-app, spa-iframe and spa-csp as packages
├── README.md
├── docs/
│ ├── GPC_POSTMESSAGE.md # Technical deep-dive on the postMessage approach
│ └── CSP_IFRAME.md # Why CSP blocks the DSR form iframe, and the fix
├── spa-app/ # Main SPA (localhost:5173)
│ ├── src/
│ │ ├── gpc.js # GPC embed helpers (loadGPCScript, reinitGPC)
│ │ ├── App.jsx
│ │ ├── main.jsx
│ │ └── pages/
│ │ ├── PageOne.jsx # Scenario 1: iframe banner only (postMessage sync)
│ │ ├── PageTwo.jsx # Scenario 2: banner on parent and iframe
│ │ └── PageThree.jsx # Scenario 3: redirect to spa-iframe
│ └── index.html
├── spa-iframe/ # Iframe host app (localhost:4000)
│ ├── src/
│ │ ├── gpc.js
│ │ ├── App.jsx
│ │ ├── main.jsx
│ │ └── pages/
│ │ ├── PageOne.jsx # Iframes back to localhost:5173
│ │ ├── PageTwo.jsx
│ │ └── PageThree.jsx
│ ├── public/
│ │ └── page5.html # Static iframe page for testing
│ └── index.html
└── spa-csp/ # CSP demo app (localhost:5174)
├── src/
│ ├── csp-policies.js # Both policies — single source of truth
│ ├── ViolationConsole.jsx # Shows CSP violations on the page itself
│ ├── App.jsx
│ ├── main.jsx
│ └── pages/
│ ├── Blocked.jsx # Strict default — iframe refused
│ └── Fixed.jsx # frame-src added — iframe loads
├── vite.config.js # Sends real CSP response headers per route
└── index.html
Install all dependencies from the repo root:
pnpm installStart both apps in parallel:
pnpm run start| App | URL |
|---|---|
| spa-app (main SPA) | http://localhost:5173 |
| spa-iframe (iframe host) | http://localhost:4000 |
| spa-csp (CSP demo) | http://localhost:5174 |
To run only the CSP demo:
pnpm run dev:spa-cspThe parent page (localhost:5173) does not run the GPC script and shows no banner. Instead it detects navigator.globalPrivacyControl and uses postMessage to pass the GPC signal and cookie state to the iframe (localhost:4000/page1.html). The iframe renders the banner.
The parent runs loadGPCScript() on mount — banner appears on the parent page. The iframe (localhost:4000/page2.html) also runs its own embed script — banner appears inside the iframe too.
Redirects to localhost:4000 (the inverse app). That app runs its own GPC script (banner on its parent page) and iframes localhost:5173 (banner inside the iframe too).
A standalone app showing why a Content-Security-Policy blocks the TrustArc DSR form iframe, and the one-line fix. Unlike the GPC scenarios, this demo needs no browser extension — it is deterministic.
Both routes are served with a real CSP response header, set per-route by middleware in spa-csp/vite.config.js. Both policies live in spa-csp/src/csp-policies.js and are imported by both the server config and the pages, so the policy displayed on screen is provably the one being enforced.
| Route | Policy | Result |
|---|---|---|
/ |
No frame-src directive |
Form iframe refused — empty box |
/fixed |
frame-src names the origin |
Form loads and is fully interactive |
The blocking is subtle: the strict policy never mentions iframes. With frame-src absent, it falls back to default-src 'self', which refuses every cross-origin frame.
default-src 'self'; script-src 'self'; style-src 'self'
← no frame-src, so default-src applies
The fix adds one directive:
frame-src https://submit-irm.trustarc.eu;
Each page also renders a live violation console (securitypolicyviolation events), so the error is visible without opening DevTools.
Note: CSP cannot be relaxed by client-side JavaScript — a policy is fixed once the document is delivered. The fix is always to serve a corrected header. See
docs/CSP_IFRAME.mdfor the full explanation, region caveats (.euvs.com), and deployment config for Netlify, Vercel and nginx.
To enable the GPC signal during testing, install the GPC Enabler Chrome extension.
Note: The extension sets
navigator.globalPrivacyControl = trueon the top-level window only. It does not inject into cross-origin iframes. Seedocs/GPC_POSTMESSAGE.mdfor how this is handled.