@c15t/react 2.0.3 → 2.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/dist/version.cjs +1 -1
- package/dist/version.js +1 -1
- package/dist-types/version.d.ts +1 -1
- package/docs/integrations/ahrefs-analytics.md +224 -0
- package/docs/integrations/cloudflare-web-analytics.md +194 -0
- package/docs/integrations/crisp.md +214 -0
- package/docs/integrations/databuddy.md +136 -65
- package/docs/integrations/fathom-analytics.md +221 -0
- package/docs/integrations/google-tag-manager.md +84 -15
- package/docs/integrations/google-tag.md +89 -8
- package/docs/integrations/hotjar.md +211 -0
- package/docs/integrations/intercom.md +214 -0
- package/docs/integrations/linkedin-insights.md +130 -11
- package/docs/integrations/matomo-analytics.md +246 -0
- package/docs/integrations/meta-pixel.md +377 -24
- package/docs/integrations/microsoft-clarity.md +241 -0
- package/docs/integrations/microsoft-uet.md +120 -9
- package/docs/integrations/mixpanel-analytics.md +198 -0
- package/docs/integrations/overview.md +69 -74
- package/docs/integrations/plausible-analytics.md +237 -0
- package/docs/integrations/posthog.md +172 -41
- package/docs/integrations/promptwatch.md +187 -0
- package/docs/integrations/reddit-pixel.md +336 -0
- package/docs/integrations/rybbit-analytics.md +222 -0
- package/docs/integrations/segment.md +213 -0
- package/docs/integrations/snapchat-pixel.md +244 -0
- package/docs/integrations/tiktok-pixel.md +88 -10
- package/docs/integrations/umami-analytics.md +220 -0
- package/docs/integrations/vercel-analytics.md +213 -0
- package/docs/integrations/x-pixel.md +99 -10
- package/docs/script-loader.md +250 -63
- package/package.json +5 -5
package/docs/script-loader.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Script Loader
|
|
3
|
-
description: Gate third-party scripts behind consent
|
|
3
|
+
description: Gate third-party scripts behind consent in React — load Google Analytics, Meta Pixel, and other tracking scripts only when users grant permission.
|
|
4
4
|
---
|
|
5
|
-
The script loader manages third-party
|
|
5
|
+
The script loader manages third-party JavaScript based on consent state. You declare scripts in your provider's `scripts` option, and c15t decides when each script should load, stay loaded, unload, or receive a consent update.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Use it for analytics, pixels, tag managers, product analytics, and other vendor snippets that should not run until the right consent condition is satisfied. Prebuilt helpers live in [`@c15t/scripts`](/docs/integrations/overview); custom scripts can be declared directly when the vendor is specific to your app.
|
|
8
8
|
|
|
9
9
|
|Package manager|Command|
|
|
10
10
|
|:--|:--|
|
|
@@ -14,17 +14,17 @@ c15t has a collection of premade scripts available in `@c15t/scripts`. Check the
|
|
|
14
14
|
|bun|`bun add @c15t/scripts`|
|
|
15
15
|
|
|
16
16
|
> ℹ️ **Info:**
|
|
17
|
-
>
|
|
17
|
+
> Start with the integrations overview before writing your own script. Built-in helpers encode vendor boot order, consent updates, and common defaults so you do not have to.
|
|
18
18
|
>
|
|
19
19
|
> ℹ️ **Info:**
|
|
20
|
-
> If you need a vendor
|
|
20
|
+
> If you need a vendor c15t does not ship yet, see the custom integration guide. It explains when a one-off Script is enough and when to build a reusable manifest-backed helper.
|
|
21
21
|
>
|
|
22
22
|
> ℹ️ **Info:**
|
|
23
|
-
> For
|
|
23
|
+
> The script loader handles JavaScript tags and callback lifecycles. For iframe-only embeds, use the iframe blocking pattern. For UI components such as maps or video players, combine consent state with a component-level placeholder or a dedicated renderable integration.
|
|
24
24
|
|
|
25
25
|
## Basic Usage
|
|
26
26
|
|
|
27
|
-
Pass an array of `Script` objects
|
|
27
|
+
Pass an array of scripts to `ConsentManagerProvider`. Built-in helpers from `@c15t/scripts` return plain `Script` objects, so they sit beside app-specific scripts:
|
|
28
28
|
|
|
29
29
|
```tsx
|
|
30
30
|
import { type ReactNode } from 'react';
|
|
@@ -53,30 +53,80 @@ export function ConsentManager({ children }: { children: ReactNode }) {
|
|
|
53
53
|
}
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
The provider registers those scripts when the consent runtime starts. From that point on, c15t owns the lifecycle: it checks consent, injects eligible scripts, unloads them when consent is revoked, and runs `onConsentChange` for scripts that stay loaded.
|
|
57
|
+
|
|
58
|
+
## Recommended Structure
|
|
59
|
+
|
|
60
|
+
Define your script list once and keep it next to the consent provider so vendor setup stays declarative:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { type ReactNode } from 'react';
|
|
64
|
+
import { ConsentManagerProvider } from '@c15t/react';
|
|
65
|
+
import { gtag } from '@c15t/scripts/google-tag';
|
|
66
|
+
import { metaPixel } from '@c15t/scripts/meta-pixel';
|
|
67
|
+
|
|
68
|
+
const scripts = [
|
|
69
|
+
gtag({ id: 'G-XXXXXXX' }),
|
|
70
|
+
metaPixel({ pixelId: '123456' }),
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
export function ConsentProvider({ children }: { children: ReactNode }) {
|
|
74
|
+
return (
|
|
75
|
+
<ConsentManagerProvider
|
|
76
|
+
options={{
|
|
77
|
+
mode: 'hosted',
|
|
78
|
+
backendURL: 'https://your-instance.c15t.dev',
|
|
79
|
+
scripts,
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</ConsentManagerProvider>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
If an integration is route-specific or tenant-specific, use [dynamic script management](#dynamic-script-management) instead of conditionally building this list per render.
|
|
57
89
|
|
|
58
|
-
|
|
59
|
-
* Use a manifest-backed helper in `@c15t/scripts` for reusable integrations, contributions, or anything that needs structured startup behavior.
|
|
90
|
+
## Mental Model
|
|
60
91
|
|
|
61
|
-
|
|
92
|
+
Every script you register has the same lifecycle. c15t evaluates each script against the current consent state, then drives it through a small number of states:
|
|
62
93
|
|
|
63
|
-
|
|
94
|
+
1. **Pending** — registered but waiting for consent. Nothing is in the DOM yet.
|
|
95
|
+
2. **Loaded** — consent matched, c15t injected the script (or ran callbacks for callback-only scripts).
|
|
96
|
+
3. **Updated** — already loaded, consent state changed, `onConsentChange` ran so the SDK can react.
|
|
97
|
+
4. **Unloaded** — consent was revoked. c15t removed the script element unless you opted into persistence.
|
|
64
98
|
|
|
65
|
-
|
|
99
|
+
Four lifecycle callbacks let you hook into transitions: `onBeforeLoad`, `onLoad`, `onConsentChange`, and `onError`. Two flags — [`alwaysLoad`](#always-load) and [`persistAfterConsentRevoked`](#persist-after-revocation) — change how c15t treats consent boundaries. Everything else (DOM placement, ad-block evasion, dynamic management) is a refinement on top of this core model.
|
|
100
|
+
|
|
101
|
+
## Choose the Right Approach
|
|
66
102
|
|
|
67
|
-
|
|
103
|
+
Most projects mix more than one style. Pick the smallest one that keeps consent behavior obvious:
|
|
68
104
|
|
|
69
|
-
|
|
105
|
+
|Style|Use when|
|
|
106
|
+
|--|--|
|
|
107
|
+
|**Built-in helper** from `@c15t/scripts`|c15t already ships the vendor. See the [integrations overview](/docs/integrations/overview).|
|
|
108
|
+
|**Plain `Script`**|One-off app code with simple load and callback behavior.|
|
|
109
|
+
|**Callback-only `Script`**|Another package already loaded the SDK; c15t only synchronizes consent.|
|
|
110
|
+
|**Manifest-backed helper**|Reusable vendor integration with structured setup phases, queues, stubs, or a vendor consent API.|
|
|
111
|
+
|**Iframe / renderable integration**|Vendor exposes an iframe or React component, not just a `<script>` tag.|
|
|
70
112
|
|
|
71
113
|
## Script Types
|
|
72
114
|
|
|
73
115
|
### Standard Scripts
|
|
74
116
|
|
|
75
|
-
|
|
117
|
+
Standard scripts load an external JavaScript file via a `<script>` tag. This is the default for most analytics and pixel SDKs:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
{
|
|
121
|
+
id: 'analytics',
|
|
122
|
+
src: 'https://cdn.example.com/analytics.js',
|
|
123
|
+
category: 'measurement',
|
|
124
|
+
}
|
|
125
|
+
```
|
|
76
126
|
|
|
77
127
|
### Inline Scripts
|
|
78
128
|
|
|
79
|
-
|
|
129
|
+
Inline scripts execute JavaScript from `textContent` instead of loading a URL. Use these sparingly; a manifest-backed helper is usually better for reusable vendor code.
|
|
80
130
|
|
|
81
131
|
```tsx
|
|
82
132
|
{
|
|
@@ -93,7 +143,7 @@ Execute inline JavaScript code. Use `textContent` instead of `src`:
|
|
|
93
143
|
|
|
94
144
|
### Callback-Only Scripts
|
|
95
145
|
|
|
96
|
-
|
|
146
|
+
Callback-only scripts do not inject a script tag. They run lifecycle callbacks when consent allows them to. Use this when another package has already loaded the SDK and c15t only needs to drive consent:
|
|
97
147
|
|
|
98
148
|
```tsx
|
|
99
149
|
{
|
|
@@ -115,31 +165,34 @@ Don't inject any `<script>` tag - just execute callbacks based on consent change
|
|
|
115
165
|
}
|
|
116
166
|
```
|
|
117
167
|
|
|
118
|
-
|
|
168
|
+
### Manifest-Backed Helpers
|
|
119
169
|
|
|
120
|
-
|
|
170
|
+
Built-in integrations in `@c15t/scripts` are manifest-backed. A manifest describes vendor setup as structured phases, then c15t compiles it into a `Script`. Manifests keep queue stubs, script URLs, consent signaling, and post-load work consistent across apps and they are safe to ship from a server.
|
|
121
171
|
|
|
122
|
-
|
|
123
|
-
// Simple: requires measurement consent
|
|
124
|
-
{ category: 'measurement' }
|
|
172
|
+
Use a manifest-backed helper when:
|
|
125
173
|
|
|
126
|
-
|
|
127
|
-
|
|
174
|
+
* the integration should be reused across projects,
|
|
175
|
+
* the vendor snippet has ordered setup steps,
|
|
176
|
+
* the vendor exposes a consent API,
|
|
177
|
+
* or you plan to contribute the integration back to c15t.
|
|
128
178
|
|
|
129
|
-
|
|
130
|
-
{ category: { or: ['measurement', 'marketing'] } }
|
|
131
|
-
```
|
|
179
|
+
Read the [custom integration guide](/docs/integrations/building-integrations) for the manifest contract, phases, and testing checklist.
|
|
132
180
|
|
|
133
|
-
|
|
181
|
+
### Iframe And Renderable Integrations
|
|
134
182
|
|
|
135
|
-
|
|
183
|
+
Some vendors are not just script tags. YouTube embeds, maps, calendars, and checkout widgets often need a visible component, a placeholder, or an iframe.
|
|
136
184
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
185
|
+
* For iframe-only embeds, gate the iframe `src` with the [iframe blocking](/docs/frameworks/react/iframe-blocking) pattern instead of loading a script just to hide an iframe.
|
|
186
|
+
* For SDK-backed UI, use the script loader for the shared SDK and render the component only when consent and SDK readiness agree.
|
|
187
|
+
|
|
188
|
+
## Lifecycle Callbacks
|
|
189
|
+
|
|
190
|
+
Every script supports four callbacks. Each receives a `ScriptCallbackInfo` payload (id, element, hasConsent, consents):
|
|
191
|
+
|
|
192
|
+
* `onBeforeLoad` — runs before the script tag is injected. Create globals, queues, or vendor stubs here.
|
|
193
|
+
* `onLoad` — runs after the browser loads the script. Call vendor `init()` APIs here.
|
|
194
|
+
* `onConsentChange` — runs for loaded scripts when consent changes. Forward the new consent state to the vendor SDK.
|
|
195
|
+
* `onError` — runs when the script fails to load. Record diagnostics or render a fallback.
|
|
143
196
|
|
|
144
197
|
```tsx
|
|
145
198
|
{
|
|
@@ -147,38 +200,60 @@ Every script supports four lifecycle callbacks:
|
|
|
147
200
|
src: 'https://analytics.example.com/v2.js',
|
|
148
201
|
category: 'measurement',
|
|
149
202
|
onBeforeLoad: ({ id }) => {
|
|
150
|
-
|
|
203
|
+
window.analyticsQueue = window.analyticsQueue || [];
|
|
151
204
|
},
|
|
152
|
-
onLoad: (
|
|
205
|
+
onLoad: () => {
|
|
153
206
|
window.analytics.init('my-key');
|
|
154
207
|
},
|
|
155
208
|
onError: ({ error }) => {
|
|
156
209
|
console.error('Failed to load analytics:', error);
|
|
157
210
|
},
|
|
158
|
-
onConsentChange: ({ hasConsent
|
|
211
|
+
onConsentChange: ({ hasConsent }) => {
|
|
159
212
|
window.analytics.setConsent(hasConsent);
|
|
160
213
|
},
|
|
161
214
|
}
|
|
162
215
|
```
|
|
163
216
|
|
|
164
|
-
##
|
|
217
|
+
## Consent Conditions
|
|
218
|
+
|
|
219
|
+
The `category` field accepts a `HasCondition`. It can be a single consent category or a logical expression:
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
// Simple: requires measurement consent
|
|
223
|
+
{ category: 'measurement' }
|
|
224
|
+
|
|
225
|
+
// AND: requires both measurement and marketing
|
|
226
|
+
{ category: { and: ['measurement', 'marketing'] } }
|
|
227
|
+
|
|
228
|
+
// OR: requires either measurement or marketing
|
|
229
|
+
{ category: { or: ['measurement', 'marketing'] } }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Consent categories use the same names as the rest of c15t (`necessary`, `functionality`, `experience`, `measurement`, `marketing`).
|
|
233
|
+
|
|
234
|
+
## Persistence Options
|
|
165
235
|
|
|
166
236
|
### Always Load
|
|
167
237
|
|
|
168
|
-
|
|
238
|
+
`alwaysLoad` loads the script regardless of whether its category is currently granted. Use it only when the vendor must be present early **and** has a reliable consent API of its own — Google Tag Manager with Consent Mode is the canonical example.
|
|
169
239
|
|
|
170
240
|
```tsx
|
|
171
241
|
{
|
|
172
242
|
id: 'google-tag-manager',
|
|
173
243
|
src: 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXX',
|
|
174
244
|
category: 'measurement',
|
|
175
|
-
alwaysLoad: true,
|
|
245
|
+
alwaysLoad: true,
|
|
176
246
|
}
|
|
177
247
|
```
|
|
178
248
|
|
|
249
|
+
When `alwaysLoad` is on, `onConsentChange` becomes mandatory: it is how the loaded SDK learns about every transition.
|
|
250
|
+
|
|
251
|
+
> ⚠️ **Warning:**
|
|
252
|
+
> alwaysLoad shifts compliance responsibility to the vendor integration. Make sure the script receives denied-by-default consent signals before it can track.
|
|
253
|
+
|
|
179
254
|
### Persist After Revocation
|
|
180
255
|
|
|
181
|
-
|
|
256
|
+
`persistAfterConsentRevoked` keeps a script in the page after consent is revoked instead of unloading it. Use it only when the vendor exposes a runtime consent toggle — otherwise unloading is safer because removing the element guarantees the SDK stops.
|
|
182
257
|
|
|
183
258
|
```tsx
|
|
184
259
|
{
|
|
@@ -186,59 +261,171 @@ Keep the script loaded even after consent is revoked (the page won't reload for
|
|
|
186
261
|
src: 'https://errors.example.com/track.js',
|
|
187
262
|
category: 'measurement',
|
|
188
263
|
persistAfterConsentRevoked: true,
|
|
264
|
+
onConsentChange: ({ hasConsent }) => {
|
|
265
|
+
window.ErrorTracker.setConsent(hasConsent);
|
|
266
|
+
},
|
|
189
267
|
}
|
|
190
268
|
```
|
|
191
269
|
|
|
192
|
-
|
|
270
|
+
As with `alwaysLoad`, `onConsentChange` is how the persisted SDK learns about consent updates.
|
|
271
|
+
|
|
272
|
+
### `alwaysLoad` vs `persistAfterConsentRevoked`
|
|
273
|
+
|
|
274
|
+
These two flags answer different questions. Use this table to keep them straight:
|
|
275
|
+
|
|
276
|
+
|Question|`alwaysLoad`|`persistAfterConsentRevoked`|
|
|
277
|
+
|--|--|--|
|
|
278
|
+
|Loads before consent is granted?|Yes|No (waits for consent like a normal script)|
|
|
279
|
+
|Stays loaded after consent is revoked?|Yes|Yes|
|
|
280
|
+
|Requires a vendor consent API?|Yes|Yes|
|
|
281
|
+
|
|
282
|
+
## DOM Placement
|
|
193
283
|
|
|
194
|
-
Control where
|
|
284
|
+
Control where the script is injected and whether the element id is anonymized:
|
|
195
285
|
|
|
196
286
|
```tsx
|
|
197
287
|
{
|
|
198
288
|
id: 'widget',
|
|
199
289
|
src: 'https://widget.example.com/embed.js',
|
|
200
290
|
category: 'experience',
|
|
201
|
-
target: 'body',
|
|
291
|
+
target: 'body', // 'head' (default) or 'body'
|
|
292
|
+
anonymizeId: true, // default: true, hides the c15t script id from ad blockers
|
|
293
|
+
nonce: 'abc123', // optional CSP nonce
|
|
202
294
|
}
|
|
203
295
|
```
|
|
204
296
|
|
|
205
|
-
|
|
297
|
+
Set `anonymizeId: false` only when another script or test needs a stable DOM id. Pass `nonce` when your CSP requires it; c15t applies it directly to the generated `<script>` element.
|
|
298
|
+
|
|
299
|
+
## Dynamic Management
|
|
300
|
+
|
|
301
|
+
Framework packages expose script-manager methods so integrations can be added, removed, or inspected at runtime. Use this for tenant-specific tools, feature-flagged scripts, or vendors that are configured after sign-in:
|
|
302
|
+
|
|
303
|
+
* `setScripts(scripts)` — registers script definitions and immediately evaluates them against consent.
|
|
304
|
+
* `removeScript(id)` — removes a definition and unloads its element if needed.
|
|
305
|
+
* `isScriptLoaded(id)` — returns whether c15t has loaded a script.
|
|
306
|
+
* `getLoadedScriptIds()` — returns every currently loaded script id.
|
|
307
|
+
|
|
308
|
+
Dynamic scripts should still use stable ids. If the same vendor is added repeatedly with different ids, c15t treats each call as a new script.
|
|
309
|
+
|
|
310
|
+
## Calling Vendor APIs From Your App
|
|
311
|
+
|
|
312
|
+
The script loader controls **when the vendor SDK loads**. It does not intercept calls your application code makes to that SDK afterwards. Whether your event calls are safe before consent is granted depends on the script's persistence flags:
|
|
206
313
|
|
|
207
|
-
|
|
314
|
+
|Vendor pattern|What c15t does|What your app code must do|
|
|
315
|
+
|--|--|--|
|
|
316
|
+
|Consent-gated load, unloaded on revoke (e.g. cookieless analytics)|Script not in DOM until consent granted; removed on revoke. Global is `undefined` outside that window.|**Guard every call.** Unguarded `window.vendor.track(...)` throws when the global is absent.|
|
|
317
|
+
|Consent-gated load with `persistAfterConsentRevoked` (e.g. Meta Pixel)|Script not in DOM until consent granted; stays after revoke. c15t calls vendor's consent-revoke API on revocation.|Guard calls only for the pre-initial-consent window. Once loaded, the SDK handles its own suppression.|
|
|
318
|
+
|`alwaysLoad: true` with a vendor consent API (e.g. GTM, gtag, Databuddy, PostHog)|Script in DOM on page start; c15t signals consent state through the vendor's API.|Calls are safe — the vendor SDK suppresses transmission when consent is denied.|
|
|
319
|
+
|No app-facing API (e.g. Cloudflare Web Analytics)|Script in/out of DOM based on consent. Tracking is fully automatic.|Nothing to guard.|
|
|
320
|
+
|
|
321
|
+
The safe pattern in React is to read consent state through `useConsentManager().has(category)` before calling the SDK:
|
|
208
322
|
|
|
209
323
|
```tsx
|
|
210
|
-
{
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
324
|
+
import { useCallback } from 'react';
|
|
325
|
+
import { useConsentManager } from '@c15t/react';
|
|
326
|
+
|
|
327
|
+
function useTrackSignup() {
|
|
328
|
+
const { has } = useConsentManager();
|
|
329
|
+
|
|
330
|
+
return useCallback(() => {
|
|
331
|
+
if (has('measurement')) {
|
|
332
|
+
window.fathom?.trackEvent('signup');
|
|
333
|
+
}
|
|
334
|
+
}, [has]);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function SignupButton() {
|
|
338
|
+
const trackSignup = useTrackSignup();
|
|
339
|
+
|
|
340
|
+
return <button onClick={trackSignup}>Sign up</button>;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
From non-React code, read the consent store directly:
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
import { getOrCreateConsentRuntime } from 'c15t';
|
|
348
|
+
|
|
349
|
+
const { consentStore } = getOrCreateConsentRuntime();
|
|
350
|
+
|
|
351
|
+
if (consentStore.getState().has('measurement')) {
|
|
352
|
+
window.fathom?.trackEvent('signup');
|
|
215
353
|
}
|
|
216
354
|
```
|
|
217
355
|
|
|
356
|
+
Each [integration page](/docs/integrations/overview) includes a vendor-specific **Tracking events in your app** block that names which pattern applies.
|
|
357
|
+
|
|
358
|
+
## Debugging Checklist
|
|
359
|
+
|
|
360
|
+
When a script does not behave as expected:
|
|
361
|
+
|
|
362
|
+
1. Confirm the script's `category` matches the consent that has been granted.
|
|
363
|
+
2. Check whether the script is `alwaysLoad` or consent-gated.
|
|
364
|
+
3. Confirm `onBeforeLoad` creates any globals before the vendor code reads them.
|
|
365
|
+
4. Confirm `onConsentChange` updates persisted or always-loaded scripts when consent changes.
|
|
366
|
+
5. Check whether the browser or an ad blocker blocked the request.
|
|
367
|
+
6. Use c15t devtools to inspect script lifecycle events when available.
|
|
368
|
+
|
|
218
369
|
## Dynamic Script Management
|
|
219
370
|
|
|
220
|
-
|
|
371
|
+
The shared guide above lists what the script-manager methods do. In React they are exposed through `useConsentManager()`:
|
|
221
372
|
|
|
222
373
|
```tsx
|
|
223
374
|
import { useConsentManager } from '@c15t/react';
|
|
224
375
|
|
|
225
376
|
function ScriptManager() {
|
|
226
|
-
const {
|
|
377
|
+
const {
|
|
378
|
+
setScripts,
|
|
379
|
+
removeScript,
|
|
380
|
+
isScriptLoaded,
|
|
381
|
+
getLoadedScriptIds,
|
|
382
|
+
} = useConsentManager();
|
|
383
|
+
|
|
384
|
+
// ...
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Register dynamic scripts from an effect or event handler — never directly in the render body. Effects guarantee the call runs once per dependency change and gives you a tear-down path:
|
|
389
|
+
|
|
390
|
+
```tsx
|
|
391
|
+
import { useEffect } from 'react';
|
|
392
|
+
import { useConsentManager } from '@c15t/react';
|
|
227
393
|
|
|
228
|
-
|
|
229
|
-
|
|
394
|
+
export function TenantAnalytics({ siteId }: { siteId: string }) {
|
|
395
|
+
const { setScripts, removeScript } = useConsentManager();
|
|
230
396
|
|
|
231
|
-
|
|
232
|
-
|
|
397
|
+
useEffect(() => {
|
|
398
|
+
const scriptId = `tenant-analytics-${siteId}`;
|
|
233
399
|
|
|
234
|
-
|
|
235
|
-
|
|
400
|
+
setScripts([
|
|
401
|
+
{
|
|
402
|
+
id: scriptId,
|
|
403
|
+
src: `https://cdn.example.com/${siteId}.js`,
|
|
404
|
+
category: 'measurement',
|
|
405
|
+
},
|
|
406
|
+
]);
|
|
236
407
|
|
|
237
|
-
|
|
238
|
-
|
|
408
|
+
return () => {
|
|
409
|
+
removeScript(scriptId);
|
|
410
|
+
};
|
|
411
|
+
}, [siteId, setScripts, removeScript]);
|
|
412
|
+
|
|
413
|
+
return null;
|
|
239
414
|
}
|
|
240
415
|
```
|
|
241
416
|
|
|
417
|
+
## Renderable Integrations
|
|
418
|
+
|
|
419
|
+
Some vendors need a render surface as well as a script — maps, video players, calendars, and checkout widgets all fall in this bucket.
|
|
420
|
+
|
|
421
|
+
Split the problem into three layers:
|
|
422
|
+
|
|
423
|
+
1. Use the script loader to gate and load the shared SDK once.
|
|
424
|
+
2. Use React state (or a custom hook) to render a placeholder until consent is granted.
|
|
425
|
+
3. Create the widget instance only after the SDK is ready and clean it up on unmount.
|
|
426
|
+
|
|
427
|
+
For iframe-only embeds, use the [iframe blocking](/docs/frameworks/react/iframe-blocking) pattern instead of loading a JavaScript SDK to hide an iframe. For SDK-backed widgets, treat the SDK as a singleton and each rendered component as its own instance.
|
|
428
|
+
|
|
242
429
|
## API Reference
|
|
243
430
|
|
|
244
431
|
### Script
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c15t/react",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Headless cookie banner, consent manager & preference center for React. RSC-compatible, GDPR/CCPA/LGPD/TCF, TypeScript-first.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -153,12 +153,12 @@
|
|
|
153
153
|
"not op_mini all"
|
|
154
154
|
],
|
|
155
155
|
"dependencies": {
|
|
156
|
-
"@c15t/ui": "2.0
|
|
157
|
-
"c15t": "2.
|
|
156
|
+
"@c15t/ui": "2.1.0",
|
|
157
|
+
"c15t": "2.1.0"
|
|
158
158
|
},
|
|
159
159
|
"devDependencies": {
|
|
160
|
-
"@c15t/backend": "2.0
|
|
161
|
-
"@c15t/iab": "2.
|
|
160
|
+
"@c15t/backend": "2.1.0",
|
|
161
|
+
"@c15t/iab": "2.1.0",
|
|
162
162
|
"@c15t/typescript-config": "0.0.1",
|
|
163
163
|
"@c15t/vitest-config": "1.0.0",
|
|
164
164
|
"@iabtechlabtcf/core": "^1.5.20",
|