@ecomconsult/consentkit 0.3.2
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/LICENSE +33 -0
- package/README.md +433 -0
- package/npm/core.cjs +67 -0
- package/npm/core.mjs +33 -0
- package/npm/index.cjs +80 -0
- package/npm/index.d.ts +224 -0
- package/npm/index.mjs +47 -0
- package/npm/internal-stub.mjs +83 -0
- package/npm/react.mjs +153 -0
- package/package.json +65 -0
- package/src/ck-core.js +1044 -0
- package/src/ck-locales.js +936 -0
- package/src/ck-saas.js +363 -0
- package/src/ck-ui.js +1421 -0
package/npm/index.d.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConsentKit — type declarations for the public API, config and React hook.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately dependency-free: nothing is imported from 'react', so these
|
|
5
|
+
* types check with a bare `tsc --noEmit` and no @types packages installed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Consent category. `necessary` is always granted and cannot be switched off. */
|
|
9
|
+
export type CkCategory = 'necessary' | 'functional' | 'analytics' | 'marketing';
|
|
10
|
+
|
|
11
|
+
/** Opt-in categories — everything except `necessary`. */
|
|
12
|
+
export type CkOptInCategory = 'functional' | 'analytics' | 'marketing';
|
|
13
|
+
|
|
14
|
+
/** Per-category grant map. */
|
|
15
|
+
export interface CkCategories {
|
|
16
|
+
/** Always `true`. */
|
|
17
|
+
necessary: boolean;
|
|
18
|
+
functional: boolean;
|
|
19
|
+
analytics: boolean;
|
|
20
|
+
marketing: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** How the current decision was made. `null` while undecided. */
|
|
24
|
+
export type CkMethod = 'accept_all' | 'reject_all' | 'custom' | null;
|
|
25
|
+
|
|
26
|
+
/** Public consent state, as returned by `getState()`. */
|
|
27
|
+
export interface CkState {
|
|
28
|
+
/** `false` until the visitor makes a choice — the banner shows while false. */
|
|
29
|
+
decided: boolean;
|
|
30
|
+
/** UUID of the stored decision, `null` while undecided. */
|
|
31
|
+
id: string | null;
|
|
32
|
+
/** ISO timestamp of the decision, `null` while undecided. */
|
|
33
|
+
ts: string | null;
|
|
34
|
+
/** Policy version the decision was recorded against. */
|
|
35
|
+
policyVersion: string;
|
|
36
|
+
categories: CkCategories;
|
|
37
|
+
method: CkMethod;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Argument to `accept()`: `'all'`, or an explicit per-category selection. */
|
|
41
|
+
export type CkAcceptArg = 'all' | Partial<Record<CkOptInCategory, boolean>>;
|
|
42
|
+
|
|
43
|
+
/** Banner placement. `bar` uses bottom/top; `box` uses the corner positions. */
|
|
44
|
+
export type CkLayoutType = 'bar' | 'modal' | 'box';
|
|
45
|
+
|
|
46
|
+
export type CkLayoutPosition = 'bottom' | 'top' | 'bottom-right' | 'bottom-left';
|
|
47
|
+
|
|
48
|
+
export interface CkLayoutConfig {
|
|
49
|
+
/** Default `'bar'`. Unknown combinations degrade to `bar` / `bottom`. */
|
|
50
|
+
type?: CkLayoutType;
|
|
51
|
+
/** `bar`: `'bottom'` (default) | `'top'`. `box`: `'bottom-left'` (default) | `'bottom-right'`. */
|
|
52
|
+
position?: CkLayoutPosition;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Optional override for the dark palette (v0.2). */
|
|
56
|
+
export interface CkDarkTheme {
|
|
57
|
+
bg?: string;
|
|
58
|
+
ink?: string;
|
|
59
|
+
accent?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CkThemeConfig {
|
|
63
|
+
/** Accent colour, exposed as `--ck-accent`. Default `'#2B50D8'`. */
|
|
64
|
+
accent?: string;
|
|
65
|
+
/** Corner radius, exposed as `--ck-radius`. Default `'10px'`. */
|
|
66
|
+
radius?: string;
|
|
67
|
+
/** v0.2. Default `'auto'` — follows `prefers-color-scheme`. */
|
|
68
|
+
mode?: 'auto' | 'light' | 'dark';
|
|
69
|
+
/** v0.2. Overrides the built-in dark palette. */
|
|
70
|
+
dark?: CkDarkTheme;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Whether a category is offered in the preferences panel at all. */
|
|
74
|
+
export interface CkCategoryConfig {
|
|
75
|
+
enabled?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CkCategoriesConfig {
|
|
79
|
+
functional?: CkCategoryConfig;
|
|
80
|
+
analytics?: CkCategoryConfig;
|
|
81
|
+
marketing?: CkCategoryConfig;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface CkIntegrationsConfig {
|
|
85
|
+
/** Google Consent Mode v2 signals. Default `true`. */
|
|
86
|
+
gcm?: boolean;
|
|
87
|
+
/** Push consent events to `window.dataLayer`. Default `true`. */
|
|
88
|
+
gtmDataLayer?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** One declared cookie, shown under its category in the preferences panel. */
|
|
92
|
+
export interface CkCookieTableEntry {
|
|
93
|
+
name: string;
|
|
94
|
+
category: CkCategory;
|
|
95
|
+
vendor?: string;
|
|
96
|
+
purpose?: string;
|
|
97
|
+
expiry?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Configuration accepted by `init()`. Every field is optional. */
|
|
101
|
+
export interface CkConfig {
|
|
102
|
+
/** Bump to invalidate stored decisions and re-show the banner. Default `'1'`. */
|
|
103
|
+
policyVersion?: string | number;
|
|
104
|
+
/** `'auto'` resolves from `navigator.language`. Default `'auto'`. */
|
|
105
|
+
language?: string;
|
|
106
|
+
layout?: CkLayoutConfig;
|
|
107
|
+
theme?: CkThemeConfig;
|
|
108
|
+
categories?: CkCategoriesConfig;
|
|
109
|
+
/** Lifetime of the stored decision, in days. Default `365`. */
|
|
110
|
+
consentTtlDays?: number;
|
|
111
|
+
integrations?: CkIntegrationsConfig;
|
|
112
|
+
cookieTable?: CkCookieTableEntry[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Detail payload of `ck:init`. */
|
|
116
|
+
export interface CkInitEventDetail {
|
|
117
|
+
state: CkState;
|
|
118
|
+
config: CkConfig;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Detail payload of `ck:consent` and `ck:change`. */
|
|
122
|
+
export interface CkStateEventDetail {
|
|
123
|
+
state: CkState;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The public API, also available as `window.ConsentKit`. */
|
|
127
|
+
export interface ConsentKitApi {
|
|
128
|
+
readonly version: string;
|
|
129
|
+
/** The merged, effective config. Populated by `init()`. */
|
|
130
|
+
config: CkConfig;
|
|
131
|
+
/** Idempotent. Restores stored state, then dispatches `ck:init`. */
|
|
132
|
+
init(config?: CkConfig): CkState;
|
|
133
|
+
allowed(category: CkCategory | string): boolean;
|
|
134
|
+
getState(): CkState;
|
|
135
|
+
/** `accept('all')` grants everything; an object records `method: 'custom'`. */
|
|
136
|
+
accept(choice?: CkAcceptArg): CkState;
|
|
137
|
+
rejectAll(): CkState;
|
|
138
|
+
/** Clears storage and known cookies, sets `decided:false`, sends GCM denied. */
|
|
139
|
+
withdraw(): CkState;
|
|
140
|
+
/** Dispatches `ck:ui:open-preferences`. */
|
|
141
|
+
show(): void;
|
|
142
|
+
/** Dispatches `ck:ui:close`. */
|
|
143
|
+
hide(): void;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
declare const ConsentKit: ConsentKitApi;
|
|
147
|
+
|
|
148
|
+
export default ConsentKit;
|
|
149
|
+
export { ConsentKit };
|
|
150
|
+
|
|
151
|
+
export declare function init(config?: CkConfig): CkState;
|
|
152
|
+
export declare function allowed(category: CkCategory | string): boolean;
|
|
153
|
+
export declare function getState(): CkState;
|
|
154
|
+
export declare function accept(choice?: CkAcceptArg): CkState;
|
|
155
|
+
export declare function rejectAll(): CkState;
|
|
156
|
+
export declare function withdraw(): CkState;
|
|
157
|
+
export declare function show(): void;
|
|
158
|
+
export declare function hide(): void;
|
|
159
|
+
|
|
160
|
+
declare global {
|
|
161
|
+
interface Window {
|
|
162
|
+
ConsentKit?: ConsentKitApi;
|
|
163
|
+
/** Extra locale packs contributed by `src/ck-locales.js`. */
|
|
164
|
+
__ckLocales?: Record<string, Record<string, string>>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
interface DocumentEventMap {
|
|
168
|
+
'ck:init': CustomEvent<CkInitEventDetail>;
|
|
169
|
+
'ck:consent': CustomEvent<CkStateEventDetail>;
|
|
170
|
+
'ck:change': CustomEvent<CkStateEventDetail>;
|
|
171
|
+
'ck:ui:open-preferences': CustomEvent<CkInitEventDetail>;
|
|
172
|
+
'ck:ui:close': CustomEvent<CkStateEventDetail>;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
declare module '@ecomconsult/consentkit' {
|
|
177
|
+
const ConsentKit: ConsentKitApi;
|
|
178
|
+
export default ConsentKit;
|
|
179
|
+
export { ConsentKit };
|
|
180
|
+
export function init(config?: CkConfig): CkState;
|
|
181
|
+
export function allowed(category: CkCategory | string): boolean;
|
|
182
|
+
export function getState(): CkState;
|
|
183
|
+
export function accept(choice?: CkAcceptArg): CkState;
|
|
184
|
+
export function rejectAll(): CkState;
|
|
185
|
+
export function withdraw(): CkState;
|
|
186
|
+
export function show(): void;
|
|
187
|
+
export function hide(): void;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
declare module '@ecomconsult/consentkit/core' {
|
|
191
|
+
const ConsentKit: ConsentKitApi;
|
|
192
|
+
export default ConsentKit;
|
|
193
|
+
export { ConsentKit };
|
|
194
|
+
export function init(config?: CkConfig): CkState;
|
|
195
|
+
export function allowed(category: CkCategory | string): boolean;
|
|
196
|
+
export function getState(): CkState;
|
|
197
|
+
export function accept(choice?: CkAcceptArg): CkState;
|
|
198
|
+
export function rejectAll(): CkState;
|
|
199
|
+
export function withdraw(): CkState;
|
|
200
|
+
export function show(): void;
|
|
201
|
+
export function hide(): void;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Return value of `useConsent()`. */
|
|
205
|
+
export interface UseConsentResult {
|
|
206
|
+
/** Current state. On the server, an undecided snapshot. */
|
|
207
|
+
state: CkState;
|
|
208
|
+
allowed(category: CkCategory | string): boolean;
|
|
209
|
+
/** Defaults to `'all'` when called with no argument. */
|
|
210
|
+
accept(choice?: CkAcceptArg): CkState;
|
|
211
|
+
rejectAll(): CkState;
|
|
212
|
+
withdraw(): CkState;
|
|
213
|
+
/** Opens the preferences panel. */
|
|
214
|
+
show(): void;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
declare module '@ecomconsult/consentkit/react' {
|
|
218
|
+
/**
|
|
219
|
+
* Subscribes to `ck:init` / `ck:consent` / `ck:change` and re-renders on
|
|
220
|
+
* change. SSR-safe: returns `decided:false` and no-op actions on the server.
|
|
221
|
+
*/
|
|
222
|
+
export function useConsent(): UseConsentResult;
|
|
223
|
+
export default useConsent;
|
|
224
|
+
}
|
package/npm/index.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* consentkit — ESM entry. Side-effect: loads core, locales and UI, then
|
|
3
|
+
* re-exports the public API.
|
|
4
|
+
*
|
|
5
|
+
* Load order is contractual: ck-core.js (blocking starts at parse time) →
|
|
6
|
+
* ck-locales.js (extra language packs) → ck-ui.js (Shadow DOM layer).
|
|
7
|
+
*
|
|
8
|
+
* Only the core is DOM-optional. `src/ck-ui.js` touches `document` at module
|
|
9
|
+
* scope, so it is imported dynamically behind a `typeof document` guard —
|
|
10
|
+
* otherwise `import '@ecomconsult/consentkit'` would throw during SSR. Locales
|
|
11
|
+
* are imported dynamically too, so a build that ships without that file still
|
|
12
|
+
* works.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { resolveApi } from './internal-stub.mjs';
|
|
16
|
+
|
|
17
|
+
// Static side-effect import: the core is safe in Node and must run first.
|
|
18
|
+
import '../src/ck-core.js';
|
|
19
|
+
|
|
20
|
+
const hasDom = typeof document !== 'undefined' && typeof window !== 'undefined';
|
|
21
|
+
|
|
22
|
+
if (hasDom) {
|
|
23
|
+
// Optional: ck-locales.js may not be present in a trimmed install.
|
|
24
|
+
try {
|
|
25
|
+
await import('../src/ck-locales.js');
|
|
26
|
+
} catch (e) { /* locales are optional; ck-ui falls back to built-in en/ru */ }
|
|
27
|
+
|
|
28
|
+
// Required in the browser, fatal in Node — hence the guard above.
|
|
29
|
+
try {
|
|
30
|
+
await import('../src/ck-ui.js');
|
|
31
|
+
} catch (e) { /* UI is best-effort; the core keeps enforcing consent */ }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ConsentKit = resolveApi();
|
|
35
|
+
|
|
36
|
+
export default ConsentKit;
|
|
37
|
+
export const {
|
|
38
|
+
init,
|
|
39
|
+
allowed,
|
|
40
|
+
getState,
|
|
41
|
+
accept,
|
|
42
|
+
rejectAll,
|
|
43
|
+
withdraw,
|
|
44
|
+
show,
|
|
45
|
+
hide
|
|
46
|
+
} = ConsentKit;
|
|
47
|
+
export { ConsentKit };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Internal: locates the ConsentKit global, or hands back a safe no-op stub.
|
|
3
|
+
* Not a public entry point — no `exports` subpath maps here.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const CATEGORIES = ['necessary', 'functional', 'analytics', 'marketing'];
|
|
7
|
+
|
|
8
|
+
function emptyCategories() {
|
|
9
|
+
return { necessary: true, functional: false, analytics: false, marketing: false };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Undecided state, shape-identical to what the real core returns. */
|
|
13
|
+
export function undecidedState() {
|
|
14
|
+
return {
|
|
15
|
+
decided: false,
|
|
16
|
+
id: null,
|
|
17
|
+
ts: null,
|
|
18
|
+
policyVersion: '1',
|
|
19
|
+
categories: emptyCategories(),
|
|
20
|
+
method: null
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* No-op API with the exact public surface, for environments where the core
|
|
26
|
+
* could not attach itself. Every method is safe to call and returns the same
|
|
27
|
+
* shape as the real one, so consumer code needs no branching.
|
|
28
|
+
*/
|
|
29
|
+
export function createStub() {
|
|
30
|
+
const stub = {
|
|
31
|
+
version: '0.2.0',
|
|
32
|
+
config: {},
|
|
33
|
+
init: function () { return undecidedState(); },
|
|
34
|
+
allowed: function (cat) { return cat === 'necessary'; },
|
|
35
|
+
getState: undecidedState,
|
|
36
|
+
accept: function () { return undecidedState(); },
|
|
37
|
+
rejectAll: function () { return undecidedState(); },
|
|
38
|
+
withdraw: function () { return undecidedState(); },
|
|
39
|
+
show: function () {},
|
|
40
|
+
hide: function () {},
|
|
41
|
+
_categories: CATEGORIES.slice(),
|
|
42
|
+
_isStub: true
|
|
43
|
+
};
|
|
44
|
+
return stub;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Candidate global objects, in the order the core itself prefers them.
|
|
49
|
+
*
|
|
50
|
+
* `src/ck-core.js` binds to `typeof window !== 'undefined' ? window :
|
|
51
|
+
* globalThis`, so `window` must be checked FIRST. In a browser the two are the
|
|
52
|
+
* same object and the order is moot, but under jsdom, Web Workers or an SSR
|
|
53
|
+
* DOM shim `window` is a distinct object and the API lands only there —
|
|
54
|
+
* checking `globalThis` alone would silently fall back to the stub.
|
|
55
|
+
*/
|
|
56
|
+
function globalCandidates() {
|
|
57
|
+
const seen = [];
|
|
58
|
+
const push = (g) => { if (g && seen.indexOf(g) === -1) seen.push(g); };
|
|
59
|
+
try { if (typeof window !== 'undefined') push(window); } catch (e) { /* noop */ }
|
|
60
|
+
try { if (typeof globalThis !== 'undefined') push(globalThis); } catch (e) { /* noop */ }
|
|
61
|
+
try { if (typeof global !== 'undefined') push(global); } catch (e) { /* noop */ }
|
|
62
|
+
try { if (typeof self !== 'undefined') push(self); } catch (e) { /* noop */ }
|
|
63
|
+
return seen;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The global object, whatever the host calls it. */
|
|
67
|
+
export function getGlobal() {
|
|
68
|
+
return globalCandidates()[0];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns the live ConsentKit API. `src/ck-core.js` assigns it to the global at
|
|
73
|
+
* parse time; under ESM its module namespace is empty, so the global is the
|
|
74
|
+
* contract. Falls back to the stub only if the core is genuinely absent.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveApi() {
|
|
77
|
+
const candidates = globalCandidates();
|
|
78
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
79
|
+
const api = candidates[i].ConsentKit;
|
|
80
|
+
if (api && typeof api.getState === 'function') return api;
|
|
81
|
+
}
|
|
82
|
+
return createStub();
|
|
83
|
+
}
|
package/npm/react.mjs
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @ecomconsult/consentkit/react — `useConsent()` hook.
|
|
3
|
+
*
|
|
4
|
+
* React is a peerDependency (>=17), never a dependency: this file imports it,
|
|
5
|
+
* the host app provides it.
|
|
6
|
+
*
|
|
7
|
+
* Two details that are easy to get wrong and are handled here:
|
|
8
|
+
*
|
|
9
|
+
* 1. `ConsentKit.getState()` builds a NEW object on every call. Feeding that
|
|
10
|
+
* straight to `useSyncExternalStore` makes `Object.is` fail on every render
|
|
11
|
+
* and loops forever. The snapshot is cached in a module-level variable and
|
|
12
|
+
* only replaced when a ck:* event actually fires.
|
|
13
|
+
* 2. On the server there is no `document` to subscribe to, so `subscribe`
|
|
14
|
+
* no-ops and `getServerSnapshot` returns a frozen undecided state.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
18
|
+
|
|
19
|
+
import { resolveApi, undecidedState } from './internal-stub.mjs';
|
|
20
|
+
|
|
21
|
+
// Side-effect import of the core only. The UI layer is intentionally NOT
|
|
22
|
+
// imported here — apps that want the banner also import
|
|
23
|
+
// '@ecomconsult/consentkit'.
|
|
24
|
+
import '../src/ck-core.js';
|
|
25
|
+
|
|
26
|
+
const EVENTS = ['ck:init', 'ck:consent', 'ck:change'];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Evaluated per call, never cached at import time: a DOM shim (jsdom, a test
|
|
30
|
+
* harness, a hydration polyfill) may be installed after this module loads, and
|
|
31
|
+
* a captured `false` would strand the hook in its server branch forever.
|
|
32
|
+
*/
|
|
33
|
+
function hasDom() {
|
|
34
|
+
return typeof document !== 'undefined' && !!document.addEventListener;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Frozen, referentially stable snapshot for SSR and the first client paint. */
|
|
38
|
+
const SERVER_SNAPSHOT = Object.freeze(undecidedState());
|
|
39
|
+
|
|
40
|
+
function api() {
|
|
41
|
+
return resolveApi();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Cached snapshot. Replaced only inside the event listener, so `getSnapshot`
|
|
46
|
+
* returns a stable reference between events.
|
|
47
|
+
*/
|
|
48
|
+
let snapshot = null;
|
|
49
|
+
|
|
50
|
+
function readSnapshot() {
|
|
51
|
+
// Filled on first read rather than at import time, for the same reason.
|
|
52
|
+
if (snapshot === null) {
|
|
53
|
+
snapshot = hasDom() ? api().getState() : SERVER_SNAPSHOT;
|
|
54
|
+
}
|
|
55
|
+
return snapshot;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readServerSnapshot() {
|
|
59
|
+
return SERVER_SNAPSHOT;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Subscribes to the consent event bus. Returns the cleanup that removes every
|
|
64
|
+
* listener it added — React calls it on unmount and on re-subscribe.
|
|
65
|
+
*/
|
|
66
|
+
function subscribe(onStoreChange) {
|
|
67
|
+
if (!hasDom()) return function () {};
|
|
68
|
+
|
|
69
|
+
const handler = function () {
|
|
70
|
+
const next = api().getState();
|
|
71
|
+
// Cheap structural compare: skip the re-render when nothing really moved.
|
|
72
|
+
if (!sameState(snapshot, next)) {
|
|
73
|
+
snapshot = next;
|
|
74
|
+
onStoreChange();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
EVENTS.forEach(function (name) {
|
|
79
|
+
document.addEventListener(name, handler, false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Close the render→subscribe gap. React reads the snapshot during render but
|
|
83
|
+
// only attaches this subscription in an effect; consent restored in between
|
|
84
|
+
// (the usual `ConsentKit.init()` in a client effect dispatching ck:init)
|
|
85
|
+
// would otherwise be missed until the next event, leaving a returning
|
|
86
|
+
// visitor rendered as undecided. `sameState` keeps the reference stable when
|
|
87
|
+
// nothing actually changed, so this never forces a spurious render.
|
|
88
|
+
handler();
|
|
89
|
+
|
|
90
|
+
return function cleanup() {
|
|
91
|
+
EVENTS.forEach(function (name) {
|
|
92
|
+
document.removeEventListener(name, handler, false);
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sameState(a, b) {
|
|
98
|
+
if (a === b) return true;
|
|
99
|
+
if (!a || !b) return false;
|
|
100
|
+
if (a.decided !== b.decided || a.id !== b.id || a.ts !== b.ts) return false;
|
|
101
|
+
if (a.method !== b.method || a.policyVersion !== b.policyVersion) return false;
|
|
102
|
+
const ca = a.categories || {};
|
|
103
|
+
const cb = b.categories || {};
|
|
104
|
+
return ca.necessary === cb.necessary &&
|
|
105
|
+
ca.functional === cb.functional &&
|
|
106
|
+
ca.analytics === cb.analytics &&
|
|
107
|
+
ca.marketing === cb.marketing;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* React binding for the consent state.
|
|
112
|
+
*
|
|
113
|
+
* @returns {{
|
|
114
|
+
* state: object,
|
|
115
|
+
* allowed: (cat: string) => boolean,
|
|
116
|
+
* accept: (choice?: any) => object,
|
|
117
|
+
* rejectAll: () => object,
|
|
118
|
+
* withdraw: () => object,
|
|
119
|
+
* show: () => void
|
|
120
|
+
* }}
|
|
121
|
+
*/
|
|
122
|
+
export function useConsent() {
|
|
123
|
+
const state = useSyncExternalStore(subscribe, readSnapshot, readServerSnapshot);
|
|
124
|
+
|
|
125
|
+
const allowed = useCallback(function (cat) {
|
|
126
|
+
if (!hasDom()) return cat === 'necessary';
|
|
127
|
+
try { return api().allowed(cat); } catch (e) { return false; }
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
const accept = useCallback(function (choice) {
|
|
131
|
+
if (!hasDom()) return SERVER_SNAPSHOT;
|
|
132
|
+
return api().accept(choice === undefined ? 'all' : choice);
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
const rejectAll = useCallback(function () {
|
|
136
|
+
if (!hasDom()) return SERVER_SNAPSHOT;
|
|
137
|
+
return api().rejectAll();
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
const withdraw = useCallback(function () {
|
|
141
|
+
if (!hasDom()) return SERVER_SNAPSHOT;
|
|
142
|
+
return api().withdraw();
|
|
143
|
+
}, []);
|
|
144
|
+
|
|
145
|
+
const show = useCallback(function () {
|
|
146
|
+
if (!hasDom()) return;
|
|
147
|
+
api().show();
|
|
148
|
+
}, []);
|
|
149
|
+
|
|
150
|
+
return { state, allowed, accept, rejectAll, withdraw, show };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export default useConsent;
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ecomconsult/consentkit",
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "GDPR cookie consent core with blocking engine, Shadow DOM UI and Google Consent Mode v2. Zero dependencies, no build step.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/vermoh/ConsentKit.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://vermoh.github.io/ConsentKit/",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/vermoh/ConsentKit/issues"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"gdpr",
|
|
15
|
+
"cookie",
|
|
16
|
+
"consent",
|
|
17
|
+
"cookie-banner",
|
|
18
|
+
"consent-mode",
|
|
19
|
+
"privacy"
|
|
20
|
+
],
|
|
21
|
+
"author": "E-COM CONSULT PLUS",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"sideEffects": true,
|
|
25
|
+
"types": "./npm/index.d.ts",
|
|
26
|
+
"main": "./npm/index.cjs",
|
|
27
|
+
"module": "./npm/index.mjs",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./npm/index.d.ts",
|
|
31
|
+
"import": "./npm/index.mjs",
|
|
32
|
+
"require": "./npm/index.cjs",
|
|
33
|
+
"default": "./npm/index.mjs"
|
|
34
|
+
},
|
|
35
|
+
"./react": {
|
|
36
|
+
"types": "./npm/index.d.ts",
|
|
37
|
+
"import": "./npm/react.mjs",
|
|
38
|
+
"default": "./npm/react.mjs"
|
|
39
|
+
},
|
|
40
|
+
"./core": {
|
|
41
|
+
"types": "./npm/index.d.ts",
|
|
42
|
+
"import": "./npm/core.mjs",
|
|
43
|
+
"require": "./npm/core.cjs",
|
|
44
|
+
"default": "./npm/core.mjs"
|
|
45
|
+
},
|
|
46
|
+
"./src/*": "./src/*",
|
|
47
|
+
"./package.json": "./package.json"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"src",
|
|
51
|
+
"npm",
|
|
52
|
+
"README.md"
|
|
53
|
+
],
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"react": ">=17"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"react": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=16"
|
|
64
|
+
}
|
|
65
|
+
}
|