@consentify/core 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@ npm install @consentify/core
13
13
  ```ts
14
14
  import { createConsentify, defaultCategories } from '@consentify/core';
15
15
 
16
- const manager = createConsentify({
16
+ const consent = createConsentify({
17
17
  policy: {
18
18
  // Prefer to set a stable identifier derived from your policy document/version.
19
19
  // If omitted, a deterministic hash of categories is used.
@@ -31,11 +31,11 @@ const manager = createConsentify({
31
31
  });
32
32
 
33
33
  // Ask user... then set decisions
34
- manager.client.set({ analytics: true });
34
+ consent.client.set({ analytics: true });
35
35
 
36
36
  // Query on client
37
- const canAnalytics = manager.client.get('analytics'); // boolean
38
- const state = manager.client.get(); // { decision: 'decided' | 'unset', snapshot? }
37
+ const canAnalytics = consent.client.get('analytics'); // boolean
38
+ const state = consent.client.get(); // { decision: 'decided' | 'unset', snapshot? }
39
39
  ```
40
40
 
41
41
  ### SSR usage
@@ -44,15 +44,57 @@ Use the server API with a raw Cookie header. It never touches the DOM.
44
44
 
45
45
  ```ts
46
46
  // Read consent on the server
47
- const state = manager.server.get(request.headers.get('cookie'));
47
+ const state = consent.server.get(request.headers.get('cookie'));
48
48
  const canAnalytics = state.decision === 'decided' && !!state.snapshot.choices.analytics;
49
49
 
50
50
  // Write consent on the server (e.g., after a POST to your consent endpoint)
51
- const setCookieHeader = manager.server.set({ analytics: true }, request.headers.get('cookie'));
51
+ const setCookieHeader = consent.server.set({ analytics: true }, request.headers.get('cookie'));
52
52
  // Then attach header: res.setHeader('Set-Cookie', setCookieHeader)
53
53
 
54
54
  // Clear consent cookie on the server
55
- const clearCookieHeader = manager.server.clear();
55
+ const clearCookieHeader = consent.server.clear();
56
+ ```
57
+
58
+ ### React integration
59
+
60
+ The client API is designed to work seamlessly with React 18+ `useSyncExternalStore`:
61
+
62
+ ```tsx
63
+ import { useSyncExternalStore } from 'react';
64
+ import { createConsentify, defaultCategories, type ConsentState, type DefaultCategory } from '@consentify/core';
65
+
66
+ // Create once at module level
67
+ const consent = createConsentify({
68
+ policy: { categories: defaultCategories, identifier: 'policy-v1' },
69
+ });
70
+
71
+ // Custom hook for React
72
+ function useConsent(): ConsentState<DefaultCategory> {
73
+ return useSyncExternalStore(
74
+ consent.client.subscribe,
75
+ consent.client.get,
76
+ consent.client.getServerSnapshot
77
+ );
78
+ }
79
+
80
+ // Usage in components
81
+ function ConsentBanner() {
82
+ const state = useConsent();
83
+
84
+ if (state.decision === 'decided') return null;
85
+
86
+ return (
87
+ <div>
88
+ <p>We use cookies to improve your experience.</p>
89
+ <button onClick={() => consent.client.set({ analytics: true, marketing: true })}>
90
+ Accept All
91
+ </button>
92
+ <button onClick={() => consent.client.set({ analytics: false, marketing: false })}>
93
+ Reject Optional
94
+ </button>
95
+ </div>
96
+ );
97
+ }
56
98
  ```
57
99
 
58
100
  ### API
@@ -63,10 +105,12 @@ Creates a consent manager bound to a `policy`. Cookie is the canonical store; yo
63
105
 
64
106
  - `policy`: `{ categories: string[]; identifier: string }`
65
107
  - `client`:
66
- - `get(): ConsentState<T>` — re-reads storage and returns `{ decision: 'decided', snapshot } | { decision: 'unset' }`.
108
+ - `get(): ConsentState<T>` — returns cached consent state `{ decision: 'decided', snapshot } | { decision: 'unset' }`.
67
109
  - `get(category: 'necessary' | T): boolean` — boolean check; `'necessary'` always returns `true`.
68
- - `set(choices: Partial<Choices<T>>): void` — merges and saves; writes only if changed.
69
- - `clear(): void` — removes stored consent (cookie and any mirror).
110
+ - `set(choices: Partial<Choices<T>>): void` — merges and saves; writes only if changed; notifies subscribers.
111
+ - `clear(): void` — removes stored consent (cookie and any mirror); notifies subscribers.
112
+ - `subscribe(callback: () => void): () => void` — subscribe to state changes (for React `useSyncExternalStore`).
113
+ - `getServerSnapshot(): ConsentState<T>` — returns `{ decision: 'unset' }` for SSR hydration.
70
114
  - `server`:
71
115
  - `get(cookieHeader: string | null | undefined): ConsentState<T>` — raw Cookie header in, state out.
72
116
  - `set(choices: Partial<Choices<T>>, currentCookieHeader?: string): string` — returns `Set-Cookie` header string.
@@ -89,6 +133,7 @@ Notes:
89
133
 
90
134
  - `'necessary'` is always `true` and cannot be disabled.
91
135
  - The snapshot is invalidated automatically when the policy identity changes (identifier/hash).
136
+ - Client state is cached and subscribers are notified on changes for optimal React performance.
92
137
 
93
138
  ### Types
94
139
 
@@ -97,21 +142,52 @@ Notes:
97
142
  - `Choices<T>` — map of consent by category plus `'necessary'`.
98
143
  - `ConsentState<T>` — `{ decision: 'decided', snapshot } | { decision: 'unset' }`.
99
144
  - `defaultCategories`/`DefaultCategory` — reusable defaults.
145
+ - `StorageKind` — `'cookie' | 'localStorage'`.
100
146
 
101
147
  ### Example: custom categories
102
148
 
103
149
  ```ts
104
150
  type Cat = 'analytics' | 'ads' | 'functional';
105
- const manager = createConsentify({
106
- policy: { categories: ['analytics','ads','functional'] as const, identifier: 'policy-v1' },
151
+ const consent = createConsentify({
152
+ policy: { categories: ['analytics', 'ads', 'functional'] as const, identifier: 'policy-v1' },
107
153
  });
108
154
 
109
- manager.client.set({ analytics: true, ads: false });
110
- if (manager.client.get('analytics')) {
155
+ consent.client.set({ analytics: true, ads: false });
156
+ if (consent.client.get('analytics')) {
111
157
  // load analytics
112
158
  }
113
159
  ```
114
160
 
161
+ ### Example: Next.js App Router
162
+
163
+ ```tsx
164
+ // lib/consent.ts
165
+ import { createConsentify, defaultCategories } from '@consentify/core';
166
+
167
+ export const consent = createConsentify({
168
+ policy: { categories: defaultCategories, identifier: 'policy-v1' },
169
+ });
170
+
171
+ // app/layout.tsx (Server Component)
172
+ import { cookies } from 'next/headers';
173
+ import { consent } from '@/lib/consent';
174
+
175
+ export default async function RootLayout({ children }) {
176
+ const cookieStore = await cookies();
177
+ const state = consent.server.get(cookieStore.toString());
178
+ const canLoadAnalytics = state.decision === 'decided' && state.snapshot.choices.analytics;
179
+
180
+ return (
181
+ <html>
182
+ <body>
183
+ {children}
184
+ {canLoadAnalytics && <AnalyticsScript />}
185
+ </body>
186
+ </html>
187
+ );
188
+ }
189
+ ```
190
+
115
191
  ### Support
116
192
 
117
193
  If you find this library useful, consider supporting its development:
@@ -122,5 +198,3 @@ If you find this library useful, consider supporting its development:
122
198
  ### License
123
199
 
124
200
  MIT © 2025 [Roman Denysov](https://github.com/RomanDenysov)
125
-
126
-
package/dist/index.d.ts CHANGED
@@ -83,6 +83,8 @@ export declare function createConsentify<Cs extends readonly string[]>(init: Cre
83
83
  };
84
84
  set: (choices: Partial<Choices<ArrToUnion<Cs>>>) => void;
85
85
  clear: () => void;
86
+ subscribe: (callback: () => void) => (() => void);
87
+ getServerSnapshot: () => ConsentState<ArrToUnion<Cs>>;
86
88
  };
87
89
  };
88
90
  export declare const defaultCategories: readonly ["preferences", "analytics", "marketing", "functional", "unclassified"];
package/dist/index.js CHANGED
@@ -131,7 +131,6 @@ export function createConsentify(init) {
131
131
  const writeClientRaw = (value) => {
132
132
  const primary = firstAvailableStore();
133
133
  writeToStore(primary, value);
134
- // Mirror to cookie if requested anywhere in order and not already writing cookie
135
134
  if (primary !== 'cookie' && storageOrder.includes('cookie'))
136
135
  writeToStore('cookie', value);
137
136
  };
@@ -190,14 +189,35 @@ export function createConsentify(init) {
190
189
  return h;
191
190
  }
192
191
  };
193
- function clientGet(category) {
192
+ // ========== NEW: Subscribe pattern for React ==========
193
+ const listeners = new Set();
194
+ const unsetState = { decision: 'unset' };
195
+ let cachedState = unsetState;
196
+ const syncState = () => {
194
197
  const s = readClient();
195
- const state = s ? { decision: 'decided', snapshot: s } : { decision: 'unset' };
198
+ if (!s) {
199
+ cachedState = unsetState;
200
+ }
201
+ else {
202
+ cachedState = { decision: 'decided', snapshot: s };
203
+ }
204
+ };
205
+ const notifyListeners = () => {
206
+ listeners.forEach(cb => cb());
207
+ };
208
+ // Init cache on browser
209
+ if (isBrowser()) {
210
+ syncState();
211
+ }
212
+ function clientGet(category) {
213
+ // Return cached state for React compatibility
196
214
  if (typeof category === 'undefined')
197
- return state;
215
+ return cachedState;
198
216
  if (category === 'necessary')
199
217
  return true;
200
- return state.decision === 'decided' ? !!state.snapshot.choices[category] : false;
218
+ return cachedState.decision === 'decided'
219
+ ? !!cachedState.snapshot.choices[category]
220
+ : false;
201
221
  }
202
222
  const client = {
203
223
  get: clientGet,
@@ -209,14 +229,26 @@ export function createConsentify(init) {
209
229
  givenAt: toISO(),
210
230
  choices: normalize({ ...base, ...choices }),
211
231
  };
212
- writeClientIfChanged(next);
232
+ const changed = writeClientIfChanged(next);
233
+ if (changed) {
234
+ syncState();
235
+ notifyListeners();
236
+ }
213
237
  },
214
238
  clear: () => {
215
239
  for (const k of new Set([...storageOrder, 'cookie']))
216
240
  clearStore(k);
241
+ syncState();
242
+ notifyListeners();
243
+ },
244
+ // NEW: Subscribe for React useSyncExternalStore
245
+ subscribe: (callback) => {
246
+ listeners.add(callback);
247
+ return () => listeners.delete(callback);
217
248
  },
249
+ // NEW: Server snapshot for SSR (always unset)
250
+ getServerSnapshot: () => unsetState,
218
251
  };
219
- // ---- single entry (DX aliases)
220
252
  return {
221
253
  policy: {
222
254
  categories: init.policy.categories,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consentify/core",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "Minimal headless cookie consent SDK (TypeScript, SSR-ready, lazy-init).",
5
5
  "author": {
6
6
  "name": "Roman Denysov",