Skip to content

fix(core): stage preference toggles in a draft until save - #162

Merged
jamiedavenport merged 3 commits into
mainfrom
claude/highest-value-issue-9929e8
Jul 25, 2026
Merged

jamiedavenport merged 3 commits into
mainfrom
claude/highest-value-issue-9929e8

Conversation

@jamiedavenport

Copy link
Copy Markdown
Owner

Summary

Fixes #157. store.toggle() committed straight to live consent state, so ticking a checkbox in a preferences panel took effect before the user pressed Save: ConsentGate opened and gated scripts loaded on tick with no way to undo, and for returning visitors every tick wrote a record to storage carrying the old decidedAt with the new decisions.

toggle() now stages the flip in a new ConsentState.draft. Gating (has() / ConsentGate), persistence, and gateScript keep reading live decisions until save() promotes the draft in one step and stamps a fresh decidedAt. Any setRoute that does not land on "preferences" discards the draft, so Back genuinely abandons unsaved edits. The tanstack/wasp blog posts already described exactly this draft model — the implementation now matches them.

Changes

  • core: draft field on ConsentState; toggle() stages into it (no live commit, no persist, no source flip); save() promotes draft ?? decisions; acceptAll/acceptNecessary/reject and reprompt invalidation clear it; route changes discard it unless landing on "preferences" (banner → customise carries staged ticks forward).
  • core (breaking): toggle(key) no longer takes ActionOptions — nothing is recorded at toggle time, so the record source is named at save(). (Previously save() overwrote toggle's source anyway, so no behavior is lost.)
  • react/vue/svelte/solid/angular: per-category granted accessors read draft ?? decisions so checkboxes respond instantly pre-save; bindings that mirror ConsentState expose draft for custom panels.
  • tests: rewrote the tests that codified the old behavior (has() reflecting unsaved toggles, persist-on-toggle) and added coverage for gate-ignores-staged-toggles, no-write-before-save for returning visitors (fresh decidedAt on the saved record), draft discard on Back, draft carry-over banner → preferences, and reprompt clearing the draft.
  • docs: "Staged preferences" section in the core consent doc, a staged-semantics note in each framework doc, and the Astro blog snippet's checkbox render made draft-aware.

Reviewer notes

  • useCategory().granted intentionally reads the draft (the checkbox view) while has()/ConsentGate intentionally do not (effective consent) — that split is the fix.
  • Custom panels rendering checkboxes from raw decisions must switch to draft ?? decisions; the per-category accessors do it already.
  • Found in passing (left as is): for a returning visitor whose stored record has a non-canonical locale (pre-PS-26), the first commit after hydration — even a bare setRoute — rewrites the record with the normalized locale. Pre-existing, orthogonal to this fix.
  • Verified with vp run -r test (all 11 workspaces), vp check, and vp run -r check-types.

toggle() committed straight to live consent state, so ticking a checkbox
in a preferences panel took effect before Save: ConsentGate opened and
gated scripts loaded on tick with no way to undo, and a returning
visitor got a record written on every tick carrying the old decidedAt
with the new decisions.

toggle() now stages the flip in state.draft. Gating (has/ConsentGate),
storage, and gated scripts keep reading live decisions until save()
promotes the draft and stamps decidedAt; any route change that does not
land on "preferences" discards the draft. Per-category granted
accessors in the react/vue/svelte/solid/angular bindings read
draft ?? decisions so checkboxes still respond instantly, and toggle()
no longer takes ActionOptions (the record source is named at save()).

Closes #157
@vercel

vercel Bot commented Jul 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
op-demo Ready Ready Preview, Comment Jul 25, 2026 3:47pm
openpolicy-sh Ready Ready Preview, Comment Jul 25, 2026 3:47pm
policystack Ready Ready Preview, Comment Jul 25, 2026 3:47pm

Request Review

@jamiedavenport
jamiedavenport merged commit 279688a into main Jul 25, 2026
5 checks passed
@jamiedavenport
jamiedavenport deleted the claude/highest-value-issue-9929e8 branch July 25, 2026 15:49

This branch was successfully deployed

3 active deployments
Preview – policystack — 34cd55aa Deployed Jul 25, 2026 by vercel[bot]
Preview – op-demo — 34cd55aa Deployed Jul 25, 2026 by vercel[bot]
Preview – openpolicy-sh — 34cd55aa Deployed Jul 25, 2026 by vercel[bot]
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.

Consent store: preferences toggles mutate live state before Save (scripts load on checkbox tick)

1 participant