## Problem
Cookie banner keeps re-prompting GDPR users who denied consent or made a
partial opt-out via Privacy Settings — they can't get rid of it.
Reported by Christian Gedde-Dahl (Front SU-362240, mygame.no) and a
Supabase support engineer independently.
The March fix for FE-2648 handled the accept case — users got their
banner dismissal stomped when GTM's Usercentrics integration migrated
localStorage from `uc_settings` to `ucData`/`ucString`. But that fix
only recognized uniformly-accepted `ucData`, so any other shape
(deny-all, essentials-plus-some-tracking, partial opt-out via the
Privacy Settings modal) fell through and was treated as "no prior
decision." Banner re-prompts on every page load.
Christian's and his colleague's `ucData.consent.services` showed 13
services accepted (essentials + functional) and 4 tracking services
denied — the shape you get from toggling off the Marketing category in
Privacy Settings. Our detection ignored it.
## Changes
- `detectPriorConsent` now returns a discriminated union — `null`, `{
kind: 'uniform-accept' }`, or `{ kind: 'decisions'; decisions }` — so we
restore per-service state faithfully instead of flattening to deny-all.
- Parse `uc_settings` for the fast cross-app nav case. The old fallback
treated `uc_user_interaction === "true"` as uniform-accept, which
silently upgraded deny users (GDPR violation waiting to happen). Now the
flag is just a gate confirming the user actually interacted, and we read
real decisions from `uc_settings.services[]`.
- Extracted the post-init orchestration from `initUserCentrics` into
exported `applyPriorDecisionToSDK(UC, initialUIValues, priorDecision)`
so it's unit-testable without mocking the dynamic SDK import.
- Added a coverage cross-check: if the Usercentrics ruleset has a
non-essential service that isn't in the user's stored decisions, force a
re-prompt rather than silently defaulting the new service. Essentials
are skipped because the SDK forces them on regardless.
- Everything fails closed on partial `ucData` / `uc_settings`
corruption. Cherry-picking the valid subset would bias toward
over-consent, which is the worst-direction bias in this domain.
## Testing
27 unit tests covering `detectPriorConsent` parsing (both `ucData` and
`uc_settings` paths, fail-closed on malformed or partially-corrupt
blobs, combined scenarios) and `applyPriorDecisionToSDK` orchestration
(uniform accept, decisions with full coverage, uncovered-non-essential
compliance guard, essentials-only negative control, null fallthrough,
SDK-already-consented passthrough).
Can't fully repro on staging — CSP blocks GTM on preview, so the
`ucData` migration never fires. Same limitation as the original FE-2648
fix. Will verify in production post-merge by asking Christian to reload
and watching support ticket volume.
Reviewed twice by Codex with a full iteration between passes; final pass
found no functional blockers.
GROWTH-790
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Fail-closed validation for stored consent data: malformed, partial, or
missing entries now yield null and avoid unsafe restoration.
* Improved precedence and fallback so corrupt prior data won’t
incorrectly restore consent.
* **Refactor**
* Consent detection now returns richer prior-decision results (uniform
accept, per-service decisions, or null).
* Applying prior decisions to the SDK uses stricter coverage checks
before restoring per-service consent.
* **Tests**
* Expanded tests covering varied stored-consent shapes, gating rules,
precedence, recovery, and SDK application behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Problem
Follow-up to #44252. The previous fix handled the case where the user
waits on the page after accepting (GTM writes `ucData`), but there's a
second scenario: if the user accepts and navigates to another app
quickly (before GTM finishes), `ucData` hasn't been written yet. The SDK
on the new app overwrites `uc_user_interaction: "true"` with `"false"`
and shows the banner again.
## What changed
`hasPreviousConsentInUcData` is now `detectPriorConsent` and checks two
signals before `UC.init()` overwrites them:
1. **Slow navigation** (existing): `ucData` contains all services with
`consent: true`
2. **Fast navigation** (new): `uc_user_interaction` is `"true"` — the
SDK on the previous app wrote this but GTM hasn't had time to replace it
yet
Also adds unit tests covering both scenarios, edge cases (empty
services, malformed JSON, mixed signals), and the combined behavior.
## Testing
- 13 unit tests for `detectPriorConsent()` covering all localStorage
state combinations
- Can't fully reproduce on staging previews (CSP blocks GTM), verified
root cause via production console monitoring
Closes FE-2648
## Problem
After PR #43221 gated `TelemetryTagManager` behind consent, the EU
cookie consent banner started reappearing on every page load and when
navigating between apps (www, studio, docs).
Back in late February we changed `TelemetryTagManager` to only load when
the user has accepted consent. This was the right call for GDPR — don't
load tracking scripts before consent. But it created a chicken-and-egg
problem with how the Usercentrics SDK stores consent.
## What happened
When a user clicks Accept, the SDK writes `uc_settings` +
`uc_user_interaction: true` to localStorage. Then the GTM script loads
(now that consent is granted), and its Usercentrics integration
immediately replaces those keys with a compressed `ucString` + `ucData`
format — deleting the originals.
On the next page load, `UC.init()` only knows how to read `uc_settings`.
It can't find it (GTM deleted it), so it treats the user as brand new
and shows the banner again. Before #43221, GTM loaded on every page
unconditionally, so its integration was already present during
`UC.init()` and could interpret the compressed format.
Confirmed via production console monitoring — the exact sequence after
clicking Accept:
```
setItem("uc_settings", ...) // SDK writes consent
setItem("uc_user_interaction", "true") // SDK marks interaction
removeItem("uc_settings") // GTM deletes SDK format
removeItem("uc_user_interaction") // GTM deletes SDK format
setItem("ucString", ...) // GTM writes compressed format
setItem("ucData", ...) // GTM writes compressed format
```
## Changes
- Read `ucData` from localStorage **before** `UC.init()` to detect prior
consent in the compressed format
- If the SDK wants to show the banner but `ucData` shows all services
were previously accepted, silently re-accept instead of re-prompting
- Added try/catch around the SDK initialization (was fire-and-forget
with no error handling, any failure was completely silent)
- Error fallback also honors prior `ucData` consent if the SDK fails to
initialize
## Testing
Can't fully reproduce on staging previews because CSP blocks the GTM
script there (so the storage migration never fires). Verified the root
cause via production console monitoring with localStorage
monkey-patching, and confirmed the `ucData` format persists across page
loads on production.
Closes FE-2648
Sorted all imports in all packages, `cms`, `design-system` and
`ui-library` apps by running `pnpm format` on them.
All changes in this PR are done by the script.
* chore: add usercentrics for consent management
* client component to make next.js happy
* address feedback
* move consent state to common
* fix import
* ensure page events are sent correctly
* add feature flag provider to ui library site
* fix ui lib 500 error
* skip in test env
---------
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
Co-authored-by: Jordi Enric <jordi.err@gmail.com>