feat(consent): cookie consent banner and cookie policy - #6832
Conversation
Adds a c15t-backed consent runtime and a Sim-styled banner, mounted from the root layout only when `isHosted` is true. A self-hosted deployment never mounts the runtime, so it makes no request to Sim's consent backend and never sees the banner. The banner is a non-modal card docked bottom-left, opposite the toast stack, built from the same chrome (border, --bg, --shadow-overlay) and from Chip/Switch/Label rather than c15t's own components — the runtime is imported from `@c15t/nextjs/headless`, which ships no UI or stylesheet. "Customize" expands the same card into per-category switches instead of opening a dialog over the app. Visibility and the available actions come from the jurisdiction policy the runtime resolves, and accept and reject are rendered with identical weight.
…lignment The consent backend was blocked by our own CSP, so the runtime silently fell back to an offline policy that showed the banner to every visitor worldwide and recorded nothing. The backend origin now lives in lib/consent/constants and the CSP builder allows it from that single source. Banner: mount the runtime beside the app rather than wrapping it, behind a dynamic() boundary, so consent state cannot re-render the page tree and a self-hosted build never fetches the chunk. Align chrome with the toast card (z token, font scale, --text-body/--text-muted pairing) and mirror the light token layer the public shells pin, which a dark-theme visitor on a landing route outside ThemeProvider's forced list would otherwise miss. Read the category list from the store's own getDisplayedConsents() — the shipped defaults mark every category except necessary as display:false, so the hand-rolled filter rendered a one-row list. Docs: add /cookie-policy as a third ProsePage consumer with the cookie inventory in tables (a new table block kind on the shared primitive), cross-reference it from the Privacy Policy, and wire it into the sitemap and llms.txt. The policy promises consent can be changed at any time, so the banner can be reopened from it.
- Drop the .light DOM probe: it matched the banner's own element, so once set it could never flip back, and it went stale on a theme toggle with no navigation. The card now pins the light layer unconditionally, as every other public surface does. - Hoist the motion/style objects to module scope. - Move a chip's mr-auto into the row layout; chips carry no outer margin. - Use the shadow-overlay utility and --border rather than the legacy alias. - Render <caption> before <colgroup>, which the HTML spec requires. - Make the code formatting of a table column a renderer concern (codeColumns) instead of JSX smuggled into the row content. - Raise the table caption above body weight, and tighten comments.
The consent runtime installs a childList+subtree MutationObserver on document.body for its iframe blocker, for the life of every hosted page — including the workflow canvas — and re-scans each added subtree. Sim gates no iframes by consent, so disableAutomaticBlocking turns it off. Also: collapse the ConsentProvider passthrough into the dynamic() export; move ConsentPreferencesLink under (landing)/cookie-policy so a shell module no longer imports landing chrome; build the three cookie tables from one shape; move the table column widths into the prose chrome layer; only compute the category list when the card is expanded; drop the ConsentCategory cast; express the card width in Tailwind rather than an inline style. Comment corrections: the sibling mount is forced by ssr:false, not by re-render concerns; lib/consent/constants must stay dependency-free because next.config loads it and the browser bundles it; codeColumns exists for biome's useJsxKeyInIterable, not for React; the headless entry omits the components but the provider still injects an inert --c15t-* style block.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Introduces CSP now allows Note: GTM/GA and other tags still load before consent in this PR; blocking is explicitly deferred. Reviewed by Cursor Bugbot for commit 982474b. Configure here. |
Greptile SummaryAdds a hosted-only cookie-consent runtime and banner, a cookie-policy page backed by the shared legal-page components, and the CSP/configuration needed to reach the consent backend.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; both previously reported issues are resolved in the current code.
|
| Filename | Overview |
|---|---|
| apps/sim/app/_shell/consent/consent-banner.tsx | Implements the headless, jurisdiction-aware consent banner and preference controls. |
| apps/sim/app/_shell/consent/consent-runtime.tsx | Configures and mounts the hosted consent runtime. |
| apps/sim/app/layout.tsx | Gates the consent provider behind the existing hosted-deployment condition. |
| apps/sim/app/(landing)/cookie-policy/cookie-policy-content.tsx | Defines the cookie-policy copy and conditionally exposes preference controls only where the runtime exists. |
| apps/sim/app/(landing)/cookie-policy/consent-preferences-link.tsx | Dispatches the preference-opening event and now follows the required named-props-interface convention. |
| apps/sim/lib/core/security/csp.ts | Allows connections to the centralized consent backend origin. |
| apps/sim/app/(landing)/components/prose-page/components/legal-block-group/components/legal-block/legal-block.tsx | Extends the shared legal-page renderer with accessible reference tables. |
Sequence Diagram
sequenceDiagram
participant Browser
participant Layout as Root Layout
participant Runtime as Consent Runtime
participant Backend as Consent Backend
participant Banner as Consent Banner
Browser->>Layout: Load hosted Sim deployment
Layout->>Runtime: Mount ConsentProvider when isHosted
Runtime->>Backend: Resolve jurisdiction policy
Backend-->>Runtime: UI mode and allowed actions
Runtime-->>Banner: Show or suppress consent UI
Browser->>Banner: Accept, reject, or customize
Banner->>Runtime: Perform consent action
Runtime->>Backend: Record subject preferences
Reviews (2): Last reviewed commit: "fix(consent): address the first review r..." | Re-trigger Greptile
- Add /cookie-policy to LANDING_ROUTES. It is an app/(landing) route, and every one of those must be exempt from COEP: the header is inherited across soft navigations, so an isolated landing page navigating into /demo leaves the Cal.com booker loading uncredentialed. - Render the withdrawal control as plain text on a self-hosted deployment, where the consent runtime is never mounted and the button had no listener. - Give ConsentPreferencesLink a named props interface.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 982474b. Configure here.
Summary
Adds a cookie consent banner (c15t, hosted mode) and a
/cookie-policypage. Both are hosted-only: the root layout renders the runtime behindisHosted, alongside the existing GTM/GA gates, so a self-hosted deployment never fetches the chunk, never reaches Sim's consent backend, and never sees the banner.Banner
A non-modal card docked bottom-left, opposite the toast stack and wearing the same chrome. It never dims, blocks, or reflows the page; "Customize" expands the same card into per-category switches rather than opening a dialog over the app. Built from
Chip/Switch/Label— the runtime is imported from@c15t/nextjs/headless, which leaves c15t's own components and stylesheet out of the bundle entirely.Visibility and the available actions come from the jurisdiction policy the runtime resolves, so the banner is absent where no consent is required (verified: a US visitor gets
ui.mode: 'none', a DE/GB visitor gets the banner). Accept and reject render with identical weight on the first layer, which GDPR requires.The CSP bug this uncovered
Our own CSP blocked
connect-srcto the consent backend, and c15t's failure mode for a blocked request is silent: it falls back to an offline policy that shows the banner to every visitor worldwide and records nothing. The backend origin now lives inlib/consent/constantsandcsp.tsallows it from that single source, so the two cannot drift. Verified end to end:200 GET /initon load,200 POST /subjectson accept.Cookie policy
/cookie-policyis a third consumer of the sharedProsePageprimitive alongside Terms and Privacy, so the three share a layout and cannot drift. The cookie inventory is rendered as tables via a newtableLegalBlockkind. The inventory was enumerated empirically from a real page load rather than written from memory.Cross-referenced from the Privacy Policy (whose
lastUpdatedis bumped), and added to the sitemap,llms.txt, andllms-full.txt. Not added to the footer, per request.Type of Change
Testing
bun run type-check,bun run lint:check, and all 29check:auditspass. New unit test covers the runtime's configuration./initand/subjectsboth 200, expand/save/reject/reopen all exercised.cookie-policy-content.tsx(it is legal text — check the retention periods and provider attributions against what we actually set) and on thelighttoken-layer decision inconsent-banner.tsx.Known gaps (deliberately not in this PR)
<Script>tags that still load before consent — measured on the landing page,_gaand five X cookies are set with the banner still on screen. This is the most-enforced ePrivacy requirement and needs its own PR, because fixing it changes production analytics collection.necessaryas a purpose.scopeModeispermissiveso our three categories still render, but the audit records won't carry measurement/marketing until they're added in the dashboard.Checklist