You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Consent preference toggles are now staged. `toggle()` writes to `state.draft` instead of live decisions, and nothing is gated, persisted, or script-loaded until `save()` promotes the draft in one step — scripts no longer load on checkbox tick before "Save", and returning visitors no longer get their stored record rewritten on every tick (#157). Leaving the preferences route without saving discards the draft.
11
+
12
+
API changes: `toggle(key)` no longer accepts `ActionOptions` (name the record source at `save()` instead), and `ConsentState` gains a required `draft` field. Per-category `granted` accessors in all framework bindings read `draft ?? decisions` so checkboxes respond instantly; custom panels rendering checkboxes from raw `decisions` should apply the same merge.
Granular per-category access. Must be called inside an injection context (e.g. a component constructor or field initializer).
87
87
88
+
`toggle` stages the change and `granted()` reflects it instantly (it reads the pending `state.draft`), but nothing is applied — `has()`, `<ConsentGate>`, script gating, and storage only change when `save()` promotes the draft. Leaving the preferences route without saving discards it.
Copy file name to clipboardExpand all lines: apps/web/content/docs/consent/core.md
+9-3Lines changed: 9 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -35,6 +35,12 @@ store.acceptAll();
35
35
36
36
The store's surface: `getState()`, `subscribe()`, `acceptAll()`, `acceptNecessary()`, `reject()`, `toggle(key)`, `save()`, `setRoute()`, `has(expr)`, `getConsentRecord()`, `getPreviousRecord()`, `refreshJurisdiction()`. See [`types.ts`](./src/types.ts) for the full shape.
37
37
38
+
### Staged preferences (`state.draft`)
39
+
40
+
`toggle(key)` never changes live consent. It stages the flip in `state.draft`, and gating (`has()` / `<ConsentGate>`), storage, and gated scripts keep reading `decisions` until `save()` promotes the draft in one step and stamps `decidedAt`. Leaving the preferences flow without saving — any `setRoute` that does not land on `"preferences"` — discards the draft, so "Back" genuinely abandons unsaved edits and nothing was loaded or persisted in the meantime.
41
+
42
+
Render preference checkboxes from `draft ?? decisions` so the panel responds instantly; the framework bindings' per-category `granted` accessor does exactly this.
43
+
38
44
## Storage adapters
39
45
40
46
Decisions persist via a `StorageAdapter` passed to `createConsentStore({ adapter })`. Three adapters ship as subpath imports:
@@ -104,7 +110,7 @@ const store = createConsentStore({ categories });
104
110
// Brave (and any browser asserting GPC) starts with all opt-outs denied.
105
111
```
106
112
107
-
Once a user makes an explicit decision (`acceptAll`, `toggle`, etc.) the resulting record has `state.source === "user"` and is preserved on reload — `applyGPC` will not overwrite it.
113
+
Once a user makes an explicit decision (`acceptAll`, `save`, etc.) the resulting record has `state.source === "user"` and is preserved on reload — `applyGPC` will not overwrite it.
108
114
109
115
To scope GPC to the legally-required US states only:
110
116
@@ -158,7 +164,7 @@ type ConsentRecord = {
158
164
-`"api"` — set via a programmatic call (override with `acceptAll({ source: "api" })`, etc.).
159
165
-`"import"` — migrated from a legacy or unrecognised record.
160
166
161
-
The store infers `source` from `state.route` at the moment the decision is taken; pass `{ source }` to any action to override it.
167
+
The store infers `source` from `state.route` at the moment the decision is taken; pass `{ source }` to any decision action (`acceptAll`, `acceptNecessary`, `reject`, `save`) to override it. `toggle` takes no options — it only stages a draft, and the eventual `save` names the source.
162
168
163
169
Read the current record via `store.getConsentRecord()` (or the binding-level `useConsent().getConsentRecord()`). It returns `null` until a decision has been recorded.
164
170
@@ -184,7 +190,7 @@ store.getConsentRecord();
184
190
185
191
Records produced by older versions of Consent are tolerated on read: missing fields fall back to safe defaults, the legacy `source: "user"` flag is mapped to `"banner"`, and any other unrecognised legacy source becomes `"import"`. The next user decision rewrites the record in the v1 shape.
186
192
187
-
GPC alone does not produce a record — the visitor has not made a decision. `getConsentRecord()` keeps returning `null` until the user accepts, rejects, saves, or toggles a category.
193
+
GPC alone does not produce a record — the visitor has not made a decision. `getConsentRecord()` keeps returning `null` until the user accepts, rejects, or saves their preference changes.
`toggle` stages the change and `granted` reflects it instantly (it reads the pending `state.draft`), but nothing is applied — `has()`, `<ConsentGate>`, script gating, and storage only change when `save()` promotes the draft. Leaving the preferences route without saving discards it.
Copy file name to clipboardExpand all lines: apps/web/content/docs/consent/solid.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -63,6 +63,8 @@ function Banner() {
63
63
64
64
Granular per-category access.
65
65
66
+
`toggle` stages the change and `granted()` reflects it instantly (it reads the pending `state.draft`), but nothing is applied — `has()`, `<ConsentGate>`, script gating, and storage only change when `save()` promotes the draft. Leaving the preferences route without saving discards it.
Copy file name to clipboardExpand all lines: apps/web/content/docs/consent/svelte.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -65,6 +65,8 @@ Returns a reactive object whose properties are tracked via `$state`. Read direct
65
65
66
66
Granular per-category access.
67
67
68
+
`toggle` stages the change and `granted` reflects it instantly (it reads the pending `state.draft`), but nothing is applied — `has()`, `<ConsentGate>`, script gating, and storage only change when `save()` promotes the draft. Leaving the preferences route without saving discards it.
69
+
68
70
```svelte
69
71
<script lang="ts">
70
72
import { getCategory } from "@policystack/svelte/consent";
Granular per-category access. Returns a `granted` computed and a `toggle` action.
61
61
62
+
`toggle` stages the change and `granted` reflects it instantly (it reads the pending `state.draft`), but nothing is applied — `has()`, `<ConsentGate>`, script gating, and storage only change when `save()` promotes the draft. Leaving the preferences route without saving discards it.
63
+
62
64
```vue
63
65
<script setup lang="ts">
64
66
import { useCategory } from "@policystack/vue/consent";
0 commit comments