@c15t/react 3.1.0-canary.0 → 3.1.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
@@ -7,7 +7,7 @@
7
7
  </a>
8
8
  </p>
9
9
 
10
- # @c15t/react: React Cookie Banner and Consent Components
10
+ # @c15t/react: React cookie banner and cookie consent components
11
11
 
12
12
  <p>
13
13
  <a href="https://www.npmjs.com/package/@c15t/react"><picture><source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/npm/%40c15t%2Freact.svg?variant=outline&mode=dark"><img src="https://shieldcn.dev/npm/%40c15t%2Freact.svg?variant=outline&mode=light" alt="Latest NPM Version"></picture></a>
@@ -18,7 +18,7 @@
18
18
  <a href="https://inth.com?utm_source=npm&utm_medium=readme&utm_campaign=oss_readme&utm_content=%40c15t%2Freact"><picture><source media="(prefers-color-scheme: dark)" srcset="https://shieldcn.dev/badge/Made%20By-Inth-ffc803.svg?color=ffc803&labelTextColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAzOTMgNDAwIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTgyLjY2MiAwdjM2Ljg5NWgtNTkuMDMxdjgyLjczM2g1OS4wMzF2MzYuODkzSDI3LjQ4MnYtMzYuODkzaDU5LjAzVjM2Ljg5NWgtNTkuMDNWMHpNMzIxLjk0MSA4OS44NVYwaDM1LjM1NXYxNTYuNTIxaC0yNS43MTNsLTg2LjEzNy05MC4zNjR2OTAuMzY0aC0zNS4zNTVWMGgyNi4zNTV6Ii8%2BPHBhdGggZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzE4LjU3MSAxODUuNzE0aDc0LjI4NlY0MDBIMFYxODUuNzE0aDI3Mi44NTd2LTQ3LjE0M3ptLTI5MS4wOSAyOC45Njl2MzcuMTE4aDU4LjEzN3YxMTkuNjI4aDM2Ljg5NVYyNTEuODAxaDU4LjU4NHYtMzcuMTE4em0xODIuNjEuMjI0djE1Ni41MjJoMzYuODk0VjMxMy41OWg3My4zNDF2NTcuODM5aDM3LjExOFYyMTQuOTA3aC0zNy4xMTh2NjEuNzg4aC03My4zNDF2LTYxLjc4OHoiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg%3D%3D&valueColor=000000&mode=dark"><img src="https://shieldcn.dev/badge/Made%20By-Inth-ffc803.svg?color=ffc803&labelTextColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAzOTMgNDAwIj48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTgyLjY2MiAwdjM2Ljg5NWgtNTkuMDMxdjgyLjczM2g1OS4wMzF2MzYuODkzSDI3LjQ4MnYtMzYuODkzaDU5LjAzVjM2Ljg5NWgtNTkuMDNWMHpNMzIxLjk0MSA4OS44NVYwaDM1LjM1NXYxNTYuNTIxaC0yNS43MTNsLTg2LjEzNy05MC4zNjR2OTAuMzY0aC0zNS4zNTVWMGgyNi4zNTV6Ii8%2BPHBhdGggZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzE4LjU3MSAxODUuNzE0aDc0LjI4NlY0MDBIMFYxODUuNzE0aDI3Mi44NTd2LTQ3LjE0M3ptLTI5MS4wOSAyOC45Njl2MzcuMTE4aDU4LjEzN3YxMTkuNjI4aDM2Ljg5NVYyNTEuODAxaDU4LjU4NHYtMzcuMTE4em0xODIuNjEuMjI0djE1Ni41MjJoMzYuODk0VjMxMy41OWg3My4zNDF2NTcuODM5aDM3LjExOFYyMTQuOTA3aC0zNy4xMTh2NjEuNzg4aC03My4zNDF2LTYxLjc4OHoiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg%3D%3D&valueColor=000000&mode=light" alt="Made by Inth"></picture></a>
19
19
  </p>
20
20
 
21
- React cookie banner, consent dialog, and preference center for React 18 and 19, with headless hooks when you want your own design.
21
+ React cookie banner, consent dialog, and preference center for React 18 and 19. Each visitor's consent policy decides whether optional cookies wait for an opt-in, as EU cookie rules require, or load until the visitor opts out. Use headless hooks when you want your own design.
22
22
 
23
23
  c15t is used by [2,100+ sites](https://www.wappalyzer.com/technologies/cookie-compliance/c15t/), including expo.dev, zed.dev, unkey.dev, mintlify.com, and sazabi.com.
24
24
 
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ import{getEventDispatcher as e}from"@c15t/core/events";import{useContext as t,useMemo as r}from"react";import{ProviderServicesContext as o}from"./context.js";import{useCommittedRef as c}from"./hooks/use-committed-ref.js";let s=function(s={}){let i=t(o);if(!i)throw Error("c15t: useEventDispatcher() needs <ConsentProvider> from @c15t/react.");let{runtime:n}=i,a=c(s);return r(()=>({pageview:t=>e(n,a.current).pageview(t),track:(t,r)=>e(n,a.current).track(t,r)}),[a,n])};export{s as useEventDispatcher};
@@ -0,0 +1 @@
1
+ export{useEventDispatcher}from"../event-dispatcher.js";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export*from"./index-parts/core.js";export*from"./index-parts/hosted.js";export*from"./index-parts/offline.js";export*from"./index-parts/presets.js";export*from"./index-parts/consent-dialog.js";export*from"./index-parts/consent-banner.js";export*from"./index-parts/consent-dialog-link.js";export*from"./index-parts/consent-dialog-trigger.js";export*from"./index-parts/consent-gate.js";export*from"./index-parts/frame.js";export*from"./index-parts/draft.js";export*from"./index-parts/translations.js";export*from"./index-parts/module-hooks.js";export*from"./index-parts/provider.js";export*from"./index-parts/theme.js";export*from"./index-parts/hooks.js";
1
+ export*from"./index-parts/core.js";export*from"./index-parts/hosted.js";export*from"./index-parts/offline.js";export*from"./index-parts/presets.js";export*from"./index-parts/consent-dialog.js";export*from"./index-parts/consent-banner.js";export*from"./index-parts/consent-dialog-link.js";export*from"./index-parts/consent-dialog-trigger.js";export*from"./index-parts/consent-gate.js";export*from"./index-parts/frame.js";export*from"./index-parts/draft.js";export*from"./index-parts/event-dispatcher.js";export*from"./index-parts/translations.js";export*from"./index-parts/module-hooks.js";export*from"./index-parts/provider.js";export*from"./index-parts/theme.js";export*from"./index-parts/hooks.js";
package/dist/provider.js CHANGED
@@ -1,2 +1,2 @@
1
1
  "use client";
2
- let e,t;import{jsx as o,jsxs as r}from"react/jsx-runtime";import{applyExperimentAssignment as n,applyExperimentTheme as i,hostExperiment as s,watchRevocationReload as c}from"@c15t/core";import{createPersistence as l,readStoredRecords as a}from"@c15t/core/modules/persistence";import{createWindowDebug as d}from"@c15t/core/modules/window-debug";import{claimEarlyJourney as u,createConsentProviderRuntime as m,earlyInitModes as p,lazyRuntimeModule as h,lazyStreamPrefetch as f,readHostedMode as g,streamPrefetchWith as v}from"@c15t/core/runtime/provider";import{applyThemeSlots as y}from"@c15t/ui/utils";import{useCallback as k,useEffect as S,useLayoutEffect as b,useMemo as w,useReducer as C,useState as x,useSyncExternalStore as j}from"react";import{KernelContext as L,ProviderServicesContext as N,StreamedPrefetchContext as O}from"./context.js";import{ExternalIABProvider as P}from"./external-iab-context.js";import{useColorScheme as D}from"./hooks/use-color-scheme.js";import{useIsHydrated as R}from"./hooks/use-is-hydrated.js";import{createIframeBlockerOnDemand as _}from"./module-hooks/iframe-blocker.js";import{UNCOMMITTED_HOLD_MS as T}from"./module-hooks/network-hold.js";import{V3ThemeProvider as E}from"./theme-provider.js";let q=(e,t)=>{let o,r=!1;return(async()=>{try{let{connectConsentSource:n}=await import("@c15t/core/runtime/controls");r||(o=n(e,t))}catch{}})(),()=>{r=!0,o?.()}},B=async()=>(await import("@c15t/core/modules/script-loader")).createScriptLoader,A=function(e){let t=e.__resolveStreamedInit;return{connectConsentSource:q,createClearOnRevocation:h(async()=>(await import("@c15t/core/modules/clear-on-revocation")).createClearOnRevocation),createIframeBlocker:_,createNetworkBlocker:h(async()=>(await import("@c15t/core/modules/network-blocker")).createNetworkBlocker),createPersistence:l,createScriptLoader:h(B),createWindowDebug:d,preloadScriptLoader:e.__preloadScriptLoader?.(B),streamPrefetch:t?v(t):f,watchRevocationReload:c}},I=new WeakMap,J=new Set,K=(e,t)=>{if("hosted"in e){let o,r;return"hosted"in t&&(o=e.hosted,r=t.hosted,o.fetch===r.fetch&&o.initialData===r.initialData&&JSON.stringify(o)===JSON.stringify(r))}return"mode"in t&&e.fetch===t.fetch&&(e.mode===t.mode||e.mode.sameAs(t.mode))},F=function(o){if("function"!=typeof o)return o;let r=I.get(o);return r||(r=Object.assign(r=>e=(t=()=>o(Object.create(r,{iabEnabled:{value:void 0}})))(),{kind:o.kind}),I.set(o,r)),r},M=function(e){return{...e,mode:F(e.mode),pkg:e.__debugPkg??"@c15t/react"}},V=function(e){let{persistence:t}=e;if(!1===t||e.consentSource)return!1;let o="object"==typeof t?t:{};if(o.skipHydration)return!1;let r=o.now?o.now():Date.now(),{records:n}=a(o.storageConfig??e.storageConfig,r);return!!(n.choice||n.noticeDismissal)},W=function(e){let{journey:t}=e;return JSON.stringify({journey:t?[t.id,t.scope,t.storedChoice]:null,overrides:e.overrides,user:e.user})},z=function(e,t,o){if(!(o&&t.policyPending)||e.prefetch||e.experiment)return;let r=g(e.mode);if(r)return{source:{hosted:{...r,fetch:r.fetch??globalThis.fetch}},storedChoice:V(e)};let n=p.get(e.mode);if(!(!n||V(e))&&n.requestsInit(t.overrides))return{source:{fetch:globalThis.fetch,mode:n},storedChoice:!1}},H=new Set,G=0,Q=function(o,r){let n,i,s,{networkBlocker:c}=o,l=m(M(o),A(o)),a=o,d=!1,p=!1,h=0,f=e;G+=1;let g={expire(){J.delete(s),H.delete(g),clearTimeout(n),d=!0,l.dispose()},holdKey:!1!==o.enabled&&c&&!1!==c.enabled?JSON.stringify(c.rules):null,sequence:G};if("u"<typeof window)p=!0;else{H.add(g),n=setTimeout(g.expire,T);let e=l.kernel.getSnapshot(),c=z(o,e,r);if(c){let{source:r,storedChoice:n}=c,l=u({option:o.journey,storedChoice:n}),a=l?{journey:l,overrides:e.overrides,user:e.user}:{overrides:e.overrides,user:e.user},d=W(a);for(let e of J)e.key===d&&K(e.source,r)&&(i=e);if(!i){let e=t(),o=e.init,n=o(a);n.catch(()=>void 0);let c=!1;e.init=e=>{let t=!c&&W(e)===d;return c=!0,t?n:o(e)},i=s={key:d,source:r,transport:e},J.add(s)}}}let v=function(){if(d)return!1;if(p)return!0;if(p=!0,J.delete(i)&&Object.assign(f,i?.transport),H.delete(g),clearTimeout(n),null!==g.holdKey)for(let e of H)e.sequence<g.sequence&&e.holdKey===g.holdKey&&e.expire();return!0};return{apply(e){e!==a&&(a=e,l.update(M(e)).catch(()=>void 0))},mount(){if(!v())return null;let e=h+=1;return l.start(),()=>{queueMicrotask(()=>{h===e&&l.dispose()})}},runtime:l}},U=function(){let e=null;return{get:(t,o)=>e??=Q(t,o),reset(){e=null}}},X=e=>e+1,Y=()=>()=>void 0,Z=function(e){let[t]=x(U),[,o]=C(X,0),r=R(),n=k(()=>{t.reset(),o()},[t]);return e?{entry:t.get(e,r),rebuild:n}:void 0},$="u"<typeof document?S:b,ee=({entry:e,options:t,rebuild:o})=>($(()=>e.apply(t),[e,t]),S(()=>{let t=e.mount();return t||void o()},[e,o]),null),et=e=>{let{children:t}=e,c=e.options??{},[l]=x(()=>e.runtime),a=Z(l?void 0:c),d=a?.entry.runtime,u=l?e.runtime??l:void 0,m=d??u,p=j(d?d.subscribe:Y,()=>m.kernel,()=>m.kernel),[h]=x(()=>{var e;return l?l.experiment??s(c.experiment,(e=c.prefetch,"function"==typeof e?.then)?void 0:c.prefetch):void 0}),f=d?d.experiment:h,g=u?c.consentCategories:void 0;S(()=>{u&&void 0!==g&&u.setConsentCategories(g)},[u,g]);let{presentation:v}=c,k=w(()=>({clearRecords:()=>m.clearRecords(),getConsentCategories:()=>m.consentCategories,getPresentation:()=>n(v,f,p.getSnapshot().experiment),isStarted:()=>m.started,setLanguage:e=>m.setLanguage(e)}),[f,p,v,m]),b=!!u&&void 0!==c.callbacks;S(()=>{"production"!==process.env.NODE_ENV&&b&&console.warn("c15t ConsentProvider: `options.callbacks` is ignored when you pass `runtime`. Pass them to createConsentRuntime({ callbacks }) instead.")},[b]);let C=j(e=>p.subscribe(e),()=>p.getSnapshot().experiment,()=>p.getServerSnapshot().experiment),R=w(()=>i(c.theme,f,C),[c.theme,f,C]);S(()=>{"production"===process.env.NODE_ENV||!R||!["colors","dark","motion","radius","shadows","spacing","typography"].some(e=>e in R)||document.getElementById("c15t-theme")||console.warn("c15t: `theme` tokens are no longer turned into CSS in the browser. Render <ConsentTheme theme={theme} /> on the server, or put the CSS from generateThemeCSS() in your stylesheet. See https://c15t.com/docs/frameworks/react/customize")},[R]);let _=w(()=>({colorScheme:c.colorScheme,disableAnimation:c.disableAnimation,noStyle:c.noStyle,scrollLock:c.scrollLock,theme:R,trapFocus:c.trapFocus}),[R,c.noStyle,c.disableAnimation,c.scrollLock,c.trapFocus,c.colorScheme]),T=w(()=>({components:y(R?.slots,c.components,"className"),experiment:f,legalLinks:c.legalLinks,nonce:c.nonce,preloadDialog:c.preloadDialog,presentation:c.presentation,styles:c.styles,theme:c.theme}),[R?.slots,c.components,f,c.legalLinks,c.nonce,c.preloadDialog,c.presentation,c.styles,c.theme]);return D(c.colorScheme),o(L.Provider,{value:p,children:o(N.Provider,{value:k,children:r(E,{themeConfig:_,uiConfig:T,children:[a?o(ee,{entry:a.entry,options:c,rebuild:a.rebuild}):null,u?o(P,{runtime:u,children:t}):o(O.Provider,{value:!1===c.streamBanner?void 0:d?.streamed,children:t})]})})})};export{et as ConsentProvider};
2
+ let e,t;import{jsx as o,jsxs as r}from"react/jsx-runtime";import{applyExperimentAssignment as n,applyExperimentTheme as i,hostExperiment as s,watchRevocationReload as c}from"@c15t/core";import{createPersistence as l,readStoredRecords as a}from"@c15t/core/modules/persistence";import{createWindowDebug as d}from"@c15t/core/modules/window-debug";import{claimEarlyJourney as u,createConsentProviderRuntime as m,earlyInitModes as p,lazyRuntimeModule as h,lazyStreamPrefetch as f,readHostedMode as g,streamPrefetchWith as v}from"@c15t/core/runtime/provider";import{applyThemeSlots as y}from"@c15t/ui/utils";import{useCallback as k,useEffect as S,useLayoutEffect as b,useMemo as w,useReducer as C,useState as x,useSyncExternalStore as j}from"react";import{KernelContext as L,ProviderServicesContext as N,StreamedPrefetchContext as O}from"./context.js";import{ExternalIABProvider as P}from"./external-iab-context.js";import{useColorScheme as D}from"./hooks/use-color-scheme.js";import{useIsHydrated as R}from"./hooks/use-is-hydrated.js";import{createIframeBlockerOnDemand as _}from"./module-hooks/iframe-blocker.js";import{UNCOMMITTED_HOLD_MS as T}from"./module-hooks/network-hold.js";import{V3ThemeProvider as E}from"./theme-provider.js";let q=(e,t)=>{let o,r=!1;return(async()=>{try{let{connectConsentSource:n}=await import("@c15t/core/runtime/controls");r||(o=n(e,t))}catch{}})(),()=>{r=!0,o?.()}},B=async()=>(await import("@c15t/core/modules/script-loader")).createScriptLoader,A=function(e){let t=e.__resolveStreamedInit;return{connectConsentSource:q,createClearOnRevocation:h(async()=>(await import("@c15t/core/modules/clear-on-revocation")).createClearOnRevocation),createIframeBlocker:_,createNetworkBlocker:h(async()=>(await import("@c15t/core/modules/network-blocker")).createNetworkBlocker),createPersistence:l,createScriptLoader:h(B),createWindowDebug:d,preloadScriptLoader:e.__preloadScriptLoader?.(B),streamPrefetch:t?v(t):f,watchRevocationReload:c}},I=new WeakMap,J=new Set,K=(e,t)=>{if("hosted"in e){let o,r;return"hosted"in t&&(o=e.hosted,r=t.hosted,o.fetch===r.fetch&&o.initialData===r.initialData&&JSON.stringify(o)===JSON.stringify(r))}return"mode"in t&&e.fetch===t.fetch&&(e.mode===t.mode||e.mode.sameAs(t.mode))},F=function(o){if("function"!=typeof o)return o;let r=I.get(o);return r||(r=Object.assign(r=>e=(t=()=>o(Object.create(r,{iabEnabled:{value:void 0}})))(),{kind:o.kind}),I.set(o,r)),r},M=function(e){return{...e,mode:F(e.mode),pkg:e.__debugPkg??"@c15t/react"}},V=function(e){let{persistence:t}=e;if(!1===t||e.consentSource)return!1;let o="object"==typeof t?t:{};if(o.skipHydration)return!1;let r=o.now?o.now():Date.now(),{records:n}=a(o.storageConfig??e.storageConfig,r);return!!(n.choice||n.noticeDismissal)},W=function(e){let{journey:t}=e;return JSON.stringify({journey:t?[t.id,t.scope,t.storedChoice]:null,overrides:e.overrides,user:e.user})},z=function(e,t,o){if(!(o&&t.policyPending)||e.prefetch||e.experiment)return;let r=g(e.mode);if(r)return{source:{hosted:{...r,fetch:r.fetch??globalThis.fetch}},storedChoice:V(e)};let n=p.get(e.mode);if(!(!n||V(e))&&n.requestsInit(t.overrides))return{source:{fetch:globalThis.fetch,mode:n},storedChoice:!1}},H=new Set,G=0,Q=function(o,r){let n,i,s,{networkBlocker:c}=o,l=m(M(o),A(o)),a=o,d=!1,p=!1,h=0,f=e;G+=1;let g={expire(){J.delete(s),H.delete(g),clearTimeout(n),d=!0,l.dispose()},holdKey:!1!==o.enabled&&c&&!1!==c.enabled?JSON.stringify(c.rules):null,sequence:G};if("u"<typeof window)p=!0;else{H.add(g),n=setTimeout(g.expire,T);let e=l.kernel.getSnapshot(),c=z(o,e,r);if(c){let{source:r,storedChoice:n}=c,l=u({option:o.journey,storedChoice:n}),a=l?{journey:l,overrides:e.overrides,user:e.user}:{overrides:e.overrides,user:e.user},d=W(a);for(let e of J)e.key===d&&K(e.source,r)&&(i=e);if(!i){let e=t(),o=e.init,n=o(a);n.catch(()=>void 0);let c=!1;e.init=e=>{let t=!c&&W(e)===d;return c=!0,t?n:o(e)},i=s={key:d,source:r,transport:e},J.add(s)}}}let v=function(){if(d)return!1;if(p)return!0;if(p=!0,J.delete(i)&&Object.assign(f,i?.transport),H.delete(g),clearTimeout(n),null!==g.holdKey)for(let e of H)e.sequence<g.sequence&&e.holdKey===g.holdKey&&e.expire();return!0};return{apply(e){e!==a&&(a=e,l.update(M(e)).catch(()=>void 0))},mount(){if(!v())return null;let e=h+=1;return l.start(),()=>{queueMicrotask(()=>{h===e&&l.dispose()})}},runtime:l}},U=function(){let e=null;return{get:(t,o)=>e??=Q(t,o),reset(){e=null}}},X=e=>e+1,Y=()=>()=>void 0,Z=function(e){let[t]=x(U),[,o]=C(X,0),r=R(),n=k(()=>{t.reset(),o()},[t]);return e?{entry:t.get(e,r),rebuild:n}:void 0},$="u"<typeof document?S:b,ee=({entry:e,options:t,rebuild:o})=>($(()=>e.apply(t),[e,t]),S(()=>{let t=e.mount();return t||void o()},[e,o]),null),et=e=>{let{children:t}=e,c=e.options??{},[l]=x(()=>e.runtime),a=Z(l?void 0:c),d=a?.entry.runtime,u=l?e.runtime??l:void 0,m=d??u,p=j(d?d.subscribe:Y,()=>m.kernel,()=>m.kernel),[h]=x(()=>{var e;return l?l.experiment??s(c.experiment,(e=c.prefetch,"function"==typeof e?.then)?void 0:c.prefetch):void 0}),f=d?d.experiment:h,g=u?c.consentCategories:void 0;S(()=>{u&&void 0!==g&&u.setConsentCategories(g)},[u,g]);let{presentation:v}=c,k=w(()=>({clearRecords:()=>m.clearRecords(),getConsentCategories:()=>m.consentCategories,getPresentation:()=>n(v,f,p.getSnapshot().experiment),isStarted:()=>m.started,runtime:m,setLanguage:e=>m.setLanguage(e)}),[f,p,v,m]),b=!!u&&void 0!==c.callbacks;S(()=>{"production"!==process.env.NODE_ENV&&b&&console.warn("c15t ConsentProvider: `options.callbacks` is ignored when you pass `runtime`. Pass them to createConsentRuntime({ callbacks }) instead.")},[b]);let C=j(e=>p.subscribe(e),()=>p.getSnapshot().experiment,()=>p.getServerSnapshot().experiment),R=w(()=>i(c.theme,f,C),[c.theme,f,C]);S(()=>{"production"===process.env.NODE_ENV||!R||!["colors","dark","motion","radius","shadows","spacing","typography"].some(e=>e in R)||document.getElementById("c15t-theme")||console.warn("c15t: `theme` tokens are no longer turned into CSS in the browser. Render <ConsentTheme theme={theme} /> on the server, or put the CSS from generateThemeCSS() in your stylesheet. See https://c15t.com/docs/frameworks/react/customize")},[R]);let _=w(()=>({colorScheme:c.colorScheme,disableAnimation:c.disableAnimation,noStyle:c.noStyle,scrollLock:c.scrollLock,theme:R,trapFocus:c.trapFocus}),[R,c.noStyle,c.disableAnimation,c.scrollLock,c.trapFocus,c.colorScheme]),T=w(()=>({components:y(R?.slots,c.components,"className"),experiment:f,legalLinks:c.legalLinks,nonce:c.nonce,preloadDialog:c.preloadDialog,presentation:c.presentation,styles:c.styles,theme:c.theme}),[R?.slots,c.components,f,c.legalLinks,c.nonce,c.preloadDialog,c.presentation,c.styles,c.theme]);return D(c.colorScheme),o(L.Provider,{value:p,children:o(N.Provider,{value:k,children:r(E,{themeConfig:_,uiConfig:T,children:[a?o(ee,{entry:a.entry,options:c,rebuild:a.rebuild}):null,u?o(P,{runtime:u,children:t}):o(O.Provider,{value:!1===c.streamBanner?void 0:d?.streamed,children:t})]})})})};export{et as ConsentProvider};
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- let e="3.1.0-canary.0";export{e as version};
1
+ let e="3.1.0";export{e as version};
@@ -1,5 +1,5 @@
1
1
  import type { AllConsentNames, ConsentKernel, ConsentPresentation, ConsentSnapshot } from '@c15t/core';
2
- import type { StreamedPrefetch } from '@c15t/core/runtime';
2
+ import type { ConsentRuntime, StreamedPrefetch } from '@c15t/core/runtime';
3
3
  /**
4
4
  * React context carrying the kernel instance.
5
5
  *
@@ -16,6 +16,8 @@ export interface ProviderServices {
16
16
  getConsentCategories: () => readonly AllConsentNames[];
17
17
  /** Whether the runtime has started: records hydrated, prefetch adopted. */
18
18
  isStarted: () => boolean;
19
+ /** The runtime the provider renders, owned or borrowed. */
20
+ runtime: ConsentRuntime;
19
21
  /**
20
22
  * Store a language override and load its copy: init runs again on the
21
23
  * provider's own kernel while it is enabled, and a borrowed runtime
@@ -0,0 +1,24 @@
1
+ import type { EventDispatcher, RuntimeEventDispatcherOptions } from '@c15t/core/events';
2
+ /**
3
+ * Get the provider's event dispatcher.
4
+ *
5
+ * It delivers to the scripts the provider registered, plus any you pass, and
6
+ * checks consent when it sends, so a revoked category stops the next event.
7
+ * The returned object stays the same for the provider's lifetime, and
8
+ * pageview de-duplication survives navigation, consent changes and the
9
+ * `enabled` toggle.
10
+ *
11
+ * @param options - Extra `scripts`, SPA `pageviews` integrations and
12
+ * injectable `globals`. The latest committed options apply on each call.
13
+ * @returns A dispatcher with `track` and `pageview`.
14
+ * @throws {Error} When called outside `ConsentProvider`.
15
+ * @example
16
+ * ```tsx
17
+ * const events = useEventDispatcher({ pageviews: ['posthog'] });
18
+ * const pathname = usePathname();
19
+ * useEffect(() => events.pageview(pathname), [events, pathname]);
20
+ * ```
21
+ */
22
+ export declare const useEventDispatcher: (options?: RuntimeEventDispatcherOptions) => EventDispatcher;
23
+ export type { EventDispatcher, EventProperties } from '@c15t/core/events';
24
+ export type { RuntimeEventDispatcherOptions as UseEventDispatcherOptions };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * One group of `@c15t/react`'s public exports. The package index re-exports
3
+ * each group with `export *`, so a bundler that splits code by reachability
4
+ * (esbuild) does not treat every module the index names as part of an app
5
+ * that imports one hook: only the groups it uses are live.
6
+ */
7
+ export { useEventDispatcher } from '../event-dispatcher.js';
@@ -40,6 +40,7 @@ export type { ConsentDialogTriggerProps, ConsentDialogTriggerToolbarAction, Cons
40
40
  export type { ConsentWidgetCompoundComponent, ConsentWidgetProps, } from './components/preferences/index.js';
41
41
  export type { ConsentGateProps, FrameProps } from './components/consent-gate/index.js';
42
42
  export type { ConsentDraftHandle, ConsentDraftProviderProps, VendorDraftHandle, } from './draft.js';
43
+ export type { EventDispatcher, EventProperties, UseEventDispatcherOptions, } from './event-dispatcher.js';
43
44
  export type { UseIframeBlockerOptions, UseNetworkBlockerOptions, UsePersistenceOptions, UseScriptLoaderOptions, } from './module-hooks.js';
44
45
  export type { ConsentProviderOptions, ConsentProviderCallbacks, ConsentProviderPrefetch, ConsentProviderProps, ExternalRuntimeProviderOptions, ExternalRuntimeProviderProps, OwnedRuntimeProviderProps, } from './provider.js';
45
46
  export type { ConsentThemeProps } from './consent-theme.js';
@@ -58,6 +59,7 @@ export * from './index-parts/consent-dialog-trigger.js';
58
59
  export * from './index-parts/consent-gate.js';
59
60
  export * from './index-parts/frame.js';
60
61
  export * from './index-parts/draft.js';
62
+ export * from './index-parts/event-dispatcher.js';
61
63
  export * from './index-parts/translations.js';
62
64
  export * from './index-parts/module-hooks.js';
63
65
  export * from './index-parts/provider.js';
@@ -1 +1 @@
1
- export declare const version = "3.1.0-canary.0";
1
+ export declare const version = "3.1.0";
@@ -136,6 +136,18 @@ writes the v3 format. The upgrade guides for
136
136
  [JavaScript](https://c15t.com/docs/frameworks/javascript/upgrade-v3#keep-visitors-existing-choices)
137
137
  cover how v3 treats v2 records.
138
138
 
139
+ ## Retry interrupted saves
140
+
141
+ Before sending a recorded choice to the backend, c15t puts it in a localStorage
142
+ queue. If the page closes while the request is in flight, the next initialization
143
+ retries it with the original decision time. A successful response removes the
144
+ queued choice. A permanent rejection, such as a stale policy, removes it too.
145
+
146
+ Queueing depends on the browser allowing storage. When storage is unavailable,
147
+ c15t still attempts the request, but cannot preserve an interrupted request for
148
+ the next page. A locally remembered choice alone does not prove that the backend
149
+ received it; check the saved consent record in Inth.
150
+
139
151
  ## Keep open tabs in step
140
152
 
141
153
  Tabs and windows on the same origin (scheme, host and port) share the consent
@@ -102,6 +102,50 @@ their vendor slug. See [vendor consent](./vendor-consent.md).
102
102
  To send your own events only to allowed integrations, see
103
103
  [send events only to allowed integrations](../../integrations/overview.md#send-events-only-to-allowed-integrations).
104
104
 
105
+ ## Send your own analytics events
106
+
107
+ `useEventDispatcher()` returns the provider's event dispatcher. It sends to the
108
+ `scripts` you registered and checks consent when an event goes out. Call
109
+ `pageview` from your router with the current path:
110
+
111
+ ```tsx title="src/analytics-events.tsx"
112
+ import { useEventDispatcher } from 'c15t/react';
113
+ import { useEffect } from 'react';
114
+
115
+ /**
116
+ * Sends a PostHog pageview for each new path. Call it once, inside
117
+ * `ConsentProvider`, with your router's current path, such as
118
+ * `useLocation().pathname` from React Router.
119
+ */
120
+ export const usePageviews = (path: string) => {
121
+ const events = useEventDispatcher({ pageviews: ['posthog'] });
122
+ useEffect(() => {
123
+ events.pageview(path);
124
+ }, [events, path]);
125
+ };
126
+
127
+ export const SearchButton = ({ resultCount }: { resultCount: number }) => {
128
+ const events = useEventDispatcher();
129
+ return (
130
+ <button
131
+ type="button"
132
+ onClick={() => {
133
+ events.track('search', { resultCount });
134
+ }}
135
+ >
136
+ Search
137
+ </button>
138
+ );
139
+ };
140
+ ```
141
+
142
+ `track` sends to every registered script with an event API and drops the
143
+ event while measurement is denied. `pageview` sends only to the integrations
144
+ named in `pageviews`, and the first call records the path without sending.
145
+ Every dispatcher shares one pageview history for the provider's lifetime.
146
+ See [send events only to allowed integrations](../../integrations/overview.md#send-events-only-to-allowed-integrations)
147
+ for the supported vendors.
148
+
105
149
  ## Check the scripts
106
150
 
107
151
  Open DevTools Network in a private window, filter by each vendor's domain and
@@ -156,33 +156,56 @@ and embeds group:
156
156
 
157
157
  ## Send events only to allowed integrations
158
158
 
159
- `createEventDispatcher` from `@c15t/integrations/events` sends one named event to
160
- every registered integration that has an event API and is allowed right now.
161
- It drops events while measurement is denied, and one vendor's error does not
162
- stop delivery to the others:
163
-
164
- ```ts
165
- import { createEventDispatcher } from '@c15t/integrations/events';
166
-
167
- const events = createEventDispatcher({
168
- scripts,
169
- getSnapshot: () => runtime.kernel.getSnapshot(),
170
- pageviews: ['segment'],
171
- });
172
-
173
- events.track('docs_search', { resultCount: 4 });
174
- events.pageview(location.pathname);
159
+ The event dispatcher sends one named event to every registered integration
160
+ that has an event API and is allowed right now. It drops events while
161
+ measurement is denied, and one vendor's error does not stop delivery to the
162
+ others. Each framework gives you the provider's dispatcher, so you don't pass
163
+ the scripts or the consent snapshot yourself:
164
+
165
+ | Framework | Get the dispatcher |
166
+ | ------------------------------ | ------------------------------------------------------------------------------ |
167
+ | React, Next.js, TanStack Start | `useEventDispatcher()` from `c15t/react`, `c15t/next` or `c15t/tanstack-start` |
168
+ | Vue, Nuxt | `useEventDispatcher()` from `c15t/vue/vue-plugin`, auto-imported in Nuxt |
169
+ | Svelte, SvelteKit | `getEventDispatcher()` from `@c15t/svelte`, called inside `ConsentProvider` |
170
+ | Astro | `getEventDispatcher()` from `c15t/astro/client` |
171
+ | JavaScript | `getEventDispatcher(runtime)` from `@c15t/browser` |
172
+
173
+ ```tsx
174
+ import { useEventDispatcher } from 'c15t/react';
175
+ import { useEffect } from 'react';
176
+
177
+ export const DocsSearch = ({ path }: { path: string }) => {
178
+ const events = useEventDispatcher({ pageviews: ['posthog'] });
179
+ useEffect(() => {
180
+ events.pageview(path);
181
+ }, [events, path]);
182
+ return (
183
+ <button onClick={() => events.track('docs_search', { resultCount: 4 })}>
184
+ Search
185
+ </button>
186
+ );
187
+ };
175
188
  ```
176
189
 
177
- Pass the same `scripts` you registered and a function that returns the live
178
- consent snapshot. `track` supports Google Tag Manager, Google Tag, PostHog,
179
- Mixpanel, Segment, Hightouch, Heap, Amplitude, LogRocket, Adobe Analytics,
180
- Databuddy, Plausible, Fathom, Pirsch, Microsoft Clarity, Hotjar, Vercel
181
- Analytics, OneDollarStats, Umami, Rybbit, RudderStack and Matomo.
182
- `pageview` sends single-page-app page views only to the integrations named in
183
- `pageviews`: PostHog, Segment or Hightouch. Call it from your router's
184
- navigation hook. The first call records the path without sending, and repeated
185
- paths or hash-only changes are skipped.
190
+ The dispatcher sends to the scripts registered on the provider. Pass
191
+ `scripts` to add more. It reads consent when an event goes out, so an event
192
+ sent right after a visitor withdraws consent is dropped. Every dispatcher for
193
+ the same provider shares one pageview history, so a component that remounts
194
+ or a consent change does not resend or lose a pageview.
195
+
196
+ `track` supports Google Tag Manager, Google Tag, PostHog, Mixpanel, Segment,
197
+ Hightouch, Heap, Amplitude, LogRocket, Adobe Analytics, Databuddy, Plausible,
198
+ Fathom, Pirsch, Microsoft Clarity, Hotjar, Vercel Analytics, OneDollarStats,
199
+ Umami, Rybbit, RudderStack and Matomo. `pageview` sends single-page-app page
200
+ views only to the integrations named in `pageviews`: PostHog, Segment or
201
+ Hightouch. Call it from your router's navigation hook. The first call records
202
+ the path without sending, and repeated paths or hash-only changes are skipped.
203
+ Each framework's scripts page shows where to call it.
204
+
205
+ Without a provider, `createEventDispatcher` from `@c15t/integrations/events`
206
+ takes `scripts` and a `getSnapshot` function that returns the live consent
207
+ snapshot. Create it once and keep it: a new dispatcher starts a new pageview
208
+ history.
186
209
 
187
210
  ## Add a vendor without a helper
188
211
 
@@ -276,6 +276,14 @@ A kernel you create yourself needs a loader from
276
276
  | `'after-consent'` | Only while measurement is allowed | Calls `opt_in_capturing` at load and `opt_out_capturing` on revocation |
277
277
  | `'disabled'` | Never | None. The helper returns an empty callback-only script |
278
278
 
279
+ With `'after-consent'`, a refusal can arrive while `array.js` is still
280
+ downloading. The download keeps going after c15t removes the script, so c15t
281
+ also clears PostHog's queued init. When `array.js` runs, it starts nothing, sends
282
+ no events and writes no cookies. If the visitor allows measurement again before
283
+ `array.js` runs, PostHog starts on the same page. If `array.js` has already
284
+ run, PostHog starts on the next page load. With the default
285
+ `reloadOnConsentRevoked`, the page reloads after the refusal anyway.
286
+
279
287
  `posthog` uses the `measurement` category. With the default
280
288
  `cookieless_mode: 'on_reject'`, PostHog keeps capturing without cookies after a
281
289
  refusal. That mode needs cookieless server hashing turned on in your PostHog
package/package.json CHANGED
@@ -1,22 +1,27 @@
1
1
  {
2
2
  "name": "@c15t/react",
3
- "version": "3.1.0-canary.0",
4
- "description": "React cookie banner, consent manager, preference center, and headless CMP components with IAB TCF and Consent Mode support.",
3
+ "version": "3.1.0",
4
+ "description": "React cookie consent banner, consent dialog and preference center, plus headless hooks. A GDPR consent management platform (CMP) with Google Consent Mode v2 and IAB TCF.",
5
5
  "keywords": [
6
+ "banner",
6
7
  "ccpa",
7
8
  "cmp",
8
9
  "consent",
9
10
  "consent-banner",
10
11
  "consent-management",
11
12
  "consent-management-platform",
13
+ "consent-manager",
12
14
  "consent-manager-react",
13
15
  "consent-mode-v2",
16
+ "cookie",
14
17
  "cookie-banner",
15
18
  "cookie-consent",
16
19
  "gdpr",
20
+ "global-privacy-control",
17
21
  "google-consent-mode",
18
22
  "headless",
19
23
  "iab",
24
+ "iab-tcf",
20
25
  "lgpd",
21
26
  "preference-center",
22
27
  "privacy",
@@ -264,16 +269,16 @@
264
269
  "test:watch": "bun prebuild && vitest"
265
270
  },
266
271
  "dependencies": {
267
- "@c15t/core": "3.1.0-canary.0",
268
- "@c15t/dev-tools": "3.1.0-canary.0",
269
- "@c15t/iab": "3.1.0-canary.0",
270
- "@c15t/schema": "3.1.0-canary.0",
271
- "@c15t/translations": "3.1.0-canary.0",
272
- "@c15t/ui": "3.0.1-canary.0",
272
+ "@c15t/core": "3.1.0",
273
+ "@c15t/dev-tools": "3.1.0",
274
+ "@c15t/iab": "3.1.0",
275
+ "@c15t/schema": "3.1.0",
276
+ "@c15t/translations": "3.1.0",
277
+ "@c15t/ui": "3.0.1",
273
278
  "@types/google.maps": "3.65.3"
274
279
  },
275
280
  "devDependencies": {
276
- "@c15t/backend": "3.1.0-canary.0",
281
+ "@c15t/backend": "3.1.0",
277
282
  "@c15t/conformance": "0.0.1",
278
283
  "@c15t/typescript-config": "0.0.1",
279
284
  "@c15t/vitest-config": "1.0.0",