@c15t/react 2.2.1 → 2.3.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/AGENTS.md +6 -3
- package/README.md +1 -1
- package/dist/components/iab-consent-dialog/atoms/purpose-item.cjs +1 -1
- package/dist/components/iab-consent-dialog/atoms/purpose-item.js +1 -1
- package/dist/components/iab-consent-dialog/hooks/use-gvl-data.cjs +1 -1
- package/dist/components/iab-consent-dialog/hooks/use-gvl-data.js +1 -1
- package/dist/components/iab-consent-dialog/iab-consent-dialog.cjs +1 -1
- package/dist/components/iab-consent-dialog/iab-consent-dialog.js +1 -1
- package/dist/iab/styles.tw3.css +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.js +1 -1
- package/dist-types/components/consent-banner/components.d.ts +3 -3
- package/dist-types/components/consent-banner/consent-banner.d.ts +1 -1
- package/dist-types/components/consent-banner/index.d.ts +9 -9
- package/dist-types/components/consent-banner/policy-actions.d.ts +2 -2
- package/dist-types/components/consent-dialog/atoms/card.d.ts +5 -5
- package/dist-types/components/consent-dialog/atoms/overlay.d.ts +1 -1
- package/dist-types/components/consent-dialog/consent-dialog.d.ts +2 -2
- package/dist-types/components/consent-dialog/index.d.ts +7 -7
- package/dist-types/components/consent-dialog-link/index.d.ts +1 -1
- package/dist-types/components/consent-dialog-trigger/atoms/button.d.ts +1 -1
- package/dist-types/components/consent-dialog-trigger/atoms/icon.d.ts +1 -1
- package/dist-types/components/consent-dialog-trigger/atoms/root.d.ts +2 -2
- package/dist-types/components/consent-dialog-trigger/atoms/toolbar.d.ts +2 -2
- package/dist-types/components/consent-dialog-trigger/consent-dialog-trigger-toolbar.d.ts +1 -1
- package/dist-types/components/consent-dialog-trigger/consent-dialog-trigger.d.ts +5 -5
- package/dist-types/components/consent-dialog-trigger/index.d.ts +4 -4
- package/dist-types/components/consent-dialog-trigger/types.d.ts +1 -1
- package/dist-types/components/consent-preferences-link/consent-preferences-link.d.ts +1 -1
- package/dist-types/components/consent-preferences-link/index.d.ts +1 -1
- package/dist-types/components/consent-widget/atoms/accordion.d.ts +3 -3
- package/dist-types/components/consent-widget/atoms/button.d.ts +1 -1
- package/dist-types/components/consent-widget/atoms/footer.d.ts +1 -1
- package/dist-types/components/consent-widget/atoms/root.d.ts +1 -1
- package/dist-types/components/consent-widget/consent-widget.d.ts +1 -1
- package/dist-types/components/consent-widget/index.d.ts +11 -11
- package/dist-types/components/consent-widget/policy-actions.d.ts +2 -2
- package/dist-types/components/consent-widget/types.d.ts +1 -1
- package/dist-types/components/frame/atoms.d.ts +2 -2
- package/dist-types/components/frame/frame.d.ts +1 -1
- package/dist-types/components/frame/index.d.ts +3 -3
- package/dist-types/components/frame/types.d.ts +1 -1
- package/dist-types/components/iab-consent-banner/iab-consent-banner.d.ts +2 -2
- package/dist-types/components/iab-consent-banner/index.d.ts +19 -19
- package/dist-types/components/iab-consent-dialog/atoms/purpose-item.d.ts +21 -2
- package/dist-types/components/iab-consent-dialog/atoms/stack-item.d.ts +1 -1
- package/dist-types/components/iab-consent-dialog/atoms/vendor-list.d.ts +1 -1
- package/dist-types/components/iab-consent-dialog/hooks/use-gvl-data.d.ts +7 -1
- package/dist-types/components/iab-consent-dialog/iab-consent-dialog.d.ts +5 -4
- package/dist-types/components/iab-consent-dialog/index.d.ts +25 -25
- package/dist-types/components/iab-consent-dialog/use-iab-translations.d.ts +4 -0
- package/dist-types/components/integrations/index.d.ts +2 -2
- package/dist-types/components/integrations/shared.d.ts +1 -1
- package/dist-types/components/integrations/youtube-embed.d.ts +1 -1
- package/dist-types/components/shared/policy-actions.d.ts +2 -2
- package/dist-types/components/shared/primitives/box.d.ts +1 -1
- package/dist-types/components/shared/primitives/button.d.ts +4 -4
- package/dist-types/components/shared/primitives/button.types.d.ts +1 -1
- package/dist-types/components/shared/primitives/legal-links/index.d.ts +1 -1
- package/dist-types/components/shared/ui/accordion/accordion.d.ts +2 -2
- package/dist-types/components/shared/ui/accordion/index.d.ts +2 -2
- package/dist-types/components/shared/ui/animated-collapse/index.d.ts +2 -2
- package/dist-types/components/shared/ui/branding.d.ts +1 -1
- package/dist-types/components/shared/ui/button/button.d.ts +1 -1
- package/dist-types/components/shared/ui/button/index.d.ts +2 -2
- package/dist-types/components/shared/ui/collapsible/index.d.ts +1 -1
- package/dist-types/components/shared/ui/dialog/index.d.ts +1 -1
- package/dist-types/components/shared/ui/preference-item/index.d.ts +1 -1
- package/dist-types/components/shared/ui/switch/index.d.ts +2 -2
- package/dist-types/components/shared/ui/switch/switch.d.ts +1 -1
- package/dist-types/components/shared/ui/tabs/index.d.ts +1 -1
- package/dist-types/context/theme-context.d.ts +1 -1
- package/dist-types/headless.d.ts +9 -9
- package/dist-types/hooks/index.d.ts +12 -12
- package/dist-types/hooks/use-headless-consent-ui.d.ts +1 -1
- package/dist-types/hooks/use-headless-iab-consent-ui.d.ts +1 -1
- package/dist-types/hooks/use-styles.d.ts +1 -1
- package/dist-types/hooks/use-theme.d.ts +1 -1
- package/dist-types/iab.d.ts +4 -4
- package/dist-types/index.d.ts +20 -20
- package/dist-types/primitives/accordion.d.ts +1 -1
- package/dist-types/primitives/button.d.ts +1 -1
- package/dist-types/primitives/collapsible.d.ts +1 -1
- package/dist-types/primitives/dialog.d.ts +1 -1
- package/dist-types/primitives/preference-item.d.ts +1 -1
- package/dist-types/primitives/switch.d.ts +1 -1
- package/dist-types/primitives/tabs.d.ts +1 -1
- package/dist-types/primitives.d.ts +7 -7
- package/dist-types/providers/consent-manager-provider.d.ts +1 -1
- package/dist-types/server/fetch-ssr-data.d.ts +1 -1
- package/dist-types/server/index.d.ts +5 -5
- package/dist-types/types/theme/index.d.ts +2 -2
- package/dist-types/types/theme/style-types.d.ts +1 -1
- package/dist-types/utils/merge-styles.d.ts +1 -1
- package/dist-types/utils/test-helpers.d.ts +1 -1
- package/dist-types/utils/theme-utils.d.ts +1 -1
- package/dist-types/version.d.ts +1 -1
- package/docs/README.md +6 -3
- package/docs/frameworks/react/concepts/client-modes.md +5 -3
- package/docs/frameworks/react/concepts/consent-models.md +2 -2
- package/docs/frameworks/react/concepts/cookie-management.md +4 -0
- package/docs/frameworks/react/concepts/glossary.md +1 -1
- package/docs/frameworks/react/concepts/initialization-flow.md +3 -3
- package/docs/frameworks/react/concepts/policy-packs.md +4 -2
- package/docs/frameworks/react/hooks/use-consent-manager/overview.md +1 -1
- package/docs/frameworks/react/iab/consent-banner.md +2 -2
- package/docs/frameworks/react/iab/consent-dialog.md +12 -3
- package/docs/frameworks/react/iab/overview.md +18 -6
- package/docs/frameworks/react/iframe-blocking.md +2 -0
- package/docs/frameworks/react/policy-packs.md +1 -1
- package/docs/integrations/front-chat.md +178 -0
- package/docs/integrations/one-dollar-stats.md +118 -0
- package/docs/integrations/overview.md +3 -0
- package/docs/integrations/pinterest-tag.md +236 -0
- package/docs/shared/concepts/client-modes.md +5 -3
- package/docs/shared/concepts/consent-models.md +2 -2
- package/docs/shared/concepts/cookie-management.md +4 -0
- package/docs/shared/concepts/glossary.md +1 -1
- package/docs/shared/concepts/initialization-flow.md +3 -3
- package/docs/shared/concepts/policy-packs.md +4 -2
- package/docs/shared/react/guides/iframe-blocking.md +2 -0
- package/docs/shared/react/guides/policy-packs.md +1 -1
- package/docs/shared/react/hooks/use-consent-manager/overview.md +1 -1
- package/docs/shared/react/iab/consent-banner.md +1 -1
- package/docs/shared/react/iab/consent-dialog.md +11 -2
- package/docs/shared/react/iab/overview.md +16 -4
- package/package.json +6 -6
- package/readme.json +1 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Pinterest Tag
|
|
3
|
+
description: Track conversions and build audiences for Pinterest advertising campaigns.
|
|
4
|
+
icon: pinterest
|
|
5
|
+
group: integrations
|
|
6
|
+
---
|
|
7
|
+
Pinterest Tag is Pinterest's conversion tracking and audience building tool. It measures the actions people take on your site after seeing your Pins, so you can attribute conversions and build retargeting audiences for Pinterest Ads.
|
|
8
|
+
|
|
9
|
+
## Official Pinterest documentation
|
|
10
|
+
|
|
11
|
+
* [Install the Pinterest Tag (Pinterest Business Help)](https://help.pinterest.com/en/business/article/install-the-pinterest-tag)
|
|
12
|
+
* [Pinterest Tag event codes](https://help.pinterest.com/en/business/article/add-event-codes)
|
|
13
|
+
|
|
14
|
+
## Integrate with c15t
|
|
15
|
+
|
|
16
|
+
**React**
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { type ReactNode } from 'react';
|
|
20
|
+
import { ConsentManagerProvider } from '@c15t/react';
|
|
21
|
+
import { pinterestTag } from '@c15t/scripts/pinterest-tag';
|
|
22
|
+
|
|
23
|
+
const scripts = [pinterestTag({ tagId: '2613654212508' })];
|
|
24
|
+
|
|
25
|
+
export function ConsentProvider({ children }: { children: ReactNode }) {
|
|
26
|
+
return (
|
|
27
|
+
<ConsentManagerProvider
|
|
28
|
+
options={{
|
|
29
|
+
mode: 'hosted',
|
|
30
|
+
backendURL: 'https://your-instance.c15t.dev',
|
|
31
|
+
scripts,
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
{children}
|
|
35
|
+
</ConsentManagerProvider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Next.js**
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
'use client';
|
|
44
|
+
|
|
45
|
+
import { type ReactNode } from 'react';
|
|
46
|
+
import { ConsentManagerProvider } from '@c15t/nextjs';
|
|
47
|
+
import { pinterestTag } from '@c15t/scripts/pinterest-tag';
|
|
48
|
+
|
|
49
|
+
const scripts = [pinterestTag({ tagId: '2613654212508' })];
|
|
50
|
+
|
|
51
|
+
export function ConsentProvider({ children }: { children: ReactNode }) {
|
|
52
|
+
return (
|
|
53
|
+
<ConsentManagerProvider
|
|
54
|
+
options={{
|
|
55
|
+
mode: 'hosted',
|
|
56
|
+
backendURL: '/api/c15t',
|
|
57
|
+
scripts,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{children}
|
|
61
|
+
</ConsentManagerProvider>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**JavaScript**
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { getOrCreateConsentRuntime } from 'c15t';
|
|
70
|
+
import { pinterestTag } from '@c15t/scripts/pinterest-tag';
|
|
71
|
+
|
|
72
|
+
getOrCreateConsentRuntime({
|
|
73
|
+
mode: 'hosted',
|
|
74
|
+
backendURL: 'https://your-instance.c15t.dev',
|
|
75
|
+
scripts: [pinterestTag({ tagId: '2613654212508' })],
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## How c15t loads it
|
|
80
|
+
|
|
81
|
+
* **Category:** `marketing` (Ads & Pixels)
|
|
82
|
+
* **Loads when:** marketing consent is granted
|
|
83
|
+
* **On revocation:** [persists](/docs/frameworks/react/script-loader#persist-after-revocation) — c15t calls `pintrk('setconsent', false)`, which stops Pinterest from sending events and clears its first-party cookies without removing the script
|
|
84
|
+
|
|
85
|
+
Before the loader runs, c15t recreates Pinterest's base code: it defines the `pintrk` queue stub with `version = "3.0"`, then queues `pintrk('load', tagId)`, `pintrk('setconsent', true)`, and `pintrk('page')`. Pinterest's `core.js` replays the queue once it loads.
|
|
86
|
+
|
|
87
|
+
## Configure the integration
|
|
88
|
+
|
|
89
|
+
You can pass extra load options and disable the default page-visit event when you need finer control:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
pinterestTag({
|
|
93
|
+
tagId: '2613654212508',
|
|
94
|
+
loadOptions: {
|
|
95
|
+
em: 'hello@example.com',
|
|
96
|
+
},
|
|
97
|
+
trackPageVisit: false,
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`loadOptions` is forwarded as the third argument to `pintrk('load', tagId, loadOptions)`. Pinterest uses `em` (email) and `external_id` for enhanced match, and hashes plain values before sending them.
|
|
102
|
+
|
|
103
|
+
> ⚠️ **Warning:**
|
|
104
|
+
> Enhanced match values are personal data. Only pass em or external\_id when your application has separately obtained consent to share them with Pinterest — marketing consent alone does not cover this.
|
|
105
|
+
|
|
106
|
+
## Tracking events in your app
|
|
107
|
+
|
|
108
|
+
c15t gates the Pinterest Tag from loading until `marketing` consent is granted. After consent is granted the script stays in the DOM, and c15t calls `pintrk('setconsent', false)` if consent is later revoked, so Pinterest itself suppresses events.
|
|
109
|
+
|
|
110
|
+
This means `window.pintrk` is only defined after the user has granted marketing consent at least once. Use `pinterestTagEvent()` for typed standard and custom events — it's a no-op until `pintrk` exists, so it's safe to call before consent, and it doesn't bypass consent afterwards. Guarding with `has('marketing')` is still good practice so you don't queue events Pinterest will drop:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { useCallback } from 'react';
|
|
114
|
+
import { useConsentManager } from '@c15t/react';
|
|
115
|
+
import { pinterestTagEvent } from '@c15t/scripts/pinterest-tag';
|
|
116
|
+
|
|
117
|
+
function useTrackCheckout() {
|
|
118
|
+
const { has } = useConsentManager();
|
|
119
|
+
return useCallback(() => {
|
|
120
|
+
if (has('marketing')) {
|
|
121
|
+
pinterestTagEvent('checkout', {
|
|
122
|
+
event_id: 'eventId0001',
|
|
123
|
+
value: 99.99,
|
|
124
|
+
order_quantity: 1,
|
|
125
|
+
currency: 'USD',
|
|
126
|
+
order_id: 'X-15148',
|
|
127
|
+
line_items: [
|
|
128
|
+
{
|
|
129
|
+
product_name: 'Parker Boots',
|
|
130
|
+
product_id: '1414',
|
|
131
|
+
product_category: 'Shoes',
|
|
132
|
+
product_price: 99.99,
|
|
133
|
+
product_quantity: 1,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}, [has]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function CheckoutButton() {
|
|
142
|
+
const trackCheckout = useTrackCheckout();
|
|
143
|
+
return <button onClick={trackCheckout}>Complete checkout</button>;
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
From plain JavaScript:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { getOrCreateConsentRuntime } from 'c15t';
|
|
151
|
+
import { pinterestTagEvent } from '@c15t/scripts/pinterest-tag';
|
|
152
|
+
|
|
153
|
+
const { consentStore } = getOrCreateConsentRuntime();
|
|
154
|
+
|
|
155
|
+
if (consentStore.getState().has('marketing')) {
|
|
156
|
+
pinterestTagEvent('lead', { lead_type: 'newsletter' });
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Pass `event_id` when you also send the same event through the Pinterest Conversions API so Pinterest can deduplicate the two. Product details (`product_name`, `product_id`, `product_price`, and so on) go inside `line_items`, one entry per product — Pinterest ignores them at the top level.
|
|
161
|
+
|
|
162
|
+
### Event names
|
|
163
|
+
|
|
164
|
+
Pinterest recognises 20 event types for conversion tracking: `checkout`, `addtocart`, `pagevisit`, `signup`, `watchvideo`, `lead`, `search`, `viewcategory`, `custom`, `addpaymentinfo`, `addtowishlist`, `initiatecheckout`, `subscribe`, `viewcontent`, `contact`, `schedule`, `findlocation`, `customizeproduct`, `submitapplication`, and `starttrial`. `pinterestTagEvent()` also accepts any other string as a user-defined event, which Pinterest uses for audience targeting only — it logs a console warning for these.
|
|
165
|
+
|
|
166
|
+
Which events you need depends on the campaign type. Conversions campaigns require at least one of `signup`, `lead`, `addtocart`, or `checkout`; catalog sales, conversion insights, dynamic retargeting, and the Verified Merchant Program all require `pagevisit`, `addtocart`, and `checkout` with `currency` set. See [Add event codes](https://help.pinterest.com/en/business/article/add-event-codes) for the full matrix.
|
|
167
|
+
|
|
168
|
+
### Debugging with a callback
|
|
169
|
+
|
|
170
|
+
Pinterest accepts an optional `(didInit, error)` callback after the event data. Pass it as the third argument to `pinterestTagEvent()` to surface tag errors during development:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { pinterestTagEvent } from '@c15t/scripts/pinterest-tag';
|
|
174
|
+
|
|
175
|
+
pinterestTagEvent('lead', { lead_type: 'Newsletter' }, (didInit, error) => {
|
|
176
|
+
if (!didInit) {
|
|
177
|
+
console.error('Pinterest Tag error:', error);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Types
|
|
183
|
+
|
|
184
|
+
### PinterestTagOptions
|
|
185
|
+
|
|
186
|
+
|Property|Value|
|
|
187
|
+
|:--|:--|
|
|
188
|
+
|Type Name|\`PinterestTagOptions\`|
|
|
189
|
+
|Source Path|\`./packages/scripts/src/vendors/ads-and-pixels/pinterest-tag.ts\`|
|
|
190
|
+
|
|
191
|
+
\*ExtractedTypeTable: Could not extract "PinterestTagOptions" from "./packages/scripts/src/vendors/ads-and-pixels/pinterest-tag.ts" using base path "/home/runner/work/c15t/c15t". Verify the path/name and that the file is included by your tsconfig.\*
|
|
192
|
+
|
|
193
|
+
### PinterestTagLoadOptions
|
|
194
|
+
|
|
195
|
+
|Property|Value|
|
|
196
|
+
|:--|:--|
|
|
197
|
+
|Type Name|\`PinterestTagLoadOptions\`|
|
|
198
|
+
|Source Path|\`./packages/scripts/src/vendors/ads-and-pixels/pinterest-tag.ts\`|
|
|
199
|
+
|
|
200
|
+
\*ExtractedTypeTable: Could not extract "PinterestTagLoadOptions" from "./packages/scripts/src/vendors/ads-and-pixels/pinterest-tag.ts" using base path "/home/runner/work/c15t/c15t". Verify the path/name and that the file is included by your tsconfig.\*
|
|
201
|
+
|
|
202
|
+
### PinterestTagEventData
|
|
203
|
+
|
|
204
|
+
|Property|Value|
|
|
205
|
+
|:--|:--|
|
|
206
|
+
|Type Name|\`PinterestTagEventData\`|
|
|
207
|
+
|Source Path|\`./packages/scripts/src/vendors/ads-and-pixels/pinterest-tag.ts\`|
|
|
208
|
+
|
|
209
|
+
\*ExtractedTypeTable: Could not extract "PinterestTagEventData" from "./packages/scripts/src/vendors/ads-and-pixels/pinterest-tag.ts" using base path "/home/runner/work/c15t/c15t". Verify the path/name and that the file is included by your tsconfig.\*
|
|
210
|
+
|
|
211
|
+
### PinterestTagLineItem
|
|
212
|
+
|
|
213
|
+
|Property|Value|
|
|
214
|
+
|:--|:--|
|
|
215
|
+
|Type Name|\`PinterestTagLineItem\`|
|
|
216
|
+
|Source Path|\`./packages/scripts/src/vendors/ads-and-pixels/pinterest-tag.ts\`|
|
|
217
|
+
|
|
218
|
+
\*ExtractedTypeTable: Could not extract "PinterestTagLineItem" from "./packages/scripts/src/vendors/ads-and-pixels/pinterest-tag.ts" using base path "/home/runner/work/c15t/c15t". Verify the path/name and that the file is included by your tsconfig.\*
|
|
219
|
+
|
|
220
|
+
### PinterestTagEventCallback
|
|
221
|
+
|
|
222
|
+
|Property|Value|
|
|
223
|
+
|:--|:--|
|
|
224
|
+
|Type Name|\`PinterestTagEventCallback\`|
|
|
225
|
+
|Source Path|\`./packages/scripts/src/vendors/ads-and-pixels/pinterest-tag.ts\`|
|
|
226
|
+
|
|
227
|
+
\*ExtractedTypeTable: Could not extract "PinterestTagEventCallback" from "./packages/scripts/src/vendors/ads-and-pixels/pinterest-tag.ts" using base path "/home/runner/work/c15t/c15t". Verify the path/name and that the file is included by your tsconfig.\*
|
|
228
|
+
|
|
229
|
+
### Script
|
|
230
|
+
|
|
231
|
+
|Property|Value|
|
|
232
|
+
|:--|:--|
|
|
233
|
+
|Type Name|\`Script\`|
|
|
234
|
+
|Source Path|\`./packages/core/src/libs/script-loader/types.ts\`|
|
|
235
|
+
|
|
236
|
+
\*ExtractedTypeTable: Could not extract "Script" from "./packages/core/src/libs/script-loader/types.ts" using base path "/home/runner/work/c15t/c15t". Verify the path/name and that the file is included by your tsconfig.\*
|
|
@@ -5,7 +5,7 @@ group: reference
|
|
|
5
5
|
---
|
|
6
6
|
c15t supports three client modes that determine how consent data is stored and synchronized. Choose the mode that matches your infrastructure:
|
|
7
7
|
|
|
8
|
-
* **Hosted mode** - Recommended for production. Backend-backed consent with geolocation, centralized policy resolution, audit history, and
|
|
8
|
+
* **Hosted mode** - Recommended for production. Backend-backed consent with geolocation, centralized policy resolution, audit history, and a temporary fallback when the backend is unreachable.
|
|
9
9
|
* **Offline mode** - Browser-only storage with no network requests. Best for local development, demos, static deployments, or controlled fallback scenarios.
|
|
10
10
|
* **Custom mode** - Bring your own backend with custom endpoint handlers
|
|
11
11
|
|
|
@@ -25,7 +25,9 @@ The default mode. Connects to a c15t backend for full consent lifecycle manageme
|
|
|
25
25
|
|
|
26
26
|
1. On page load, the client calls `/init` to fetch geolocation, jurisdiction, localized translation strings
|
|
27
27
|
2. When the user grants or changes consent, it is saved locally before being synced to the backend.
|
|
28
|
-
3. If
|
|
28
|
+
3. If `/init` fails after retries, c15t uses a temporary opt-in fallback. Optional categories are denied during fallback, while the saved choice and its hosted policy fingerprint remain in browser storage.
|
|
29
|
+
|
|
30
|
+
A saved rejection survives an outage and recovery, including recovery to an opt-out or no-banner policy. A rejection saved during fallback also survives recovery. Grants saved during fallback are not sent to the backend and need fresh confirmation under the hosted policy. Previously valid grants can resume on a later load if the original hosted policy returns and the user has not changed their choice.
|
|
29
31
|
|
|
30
32
|
**Configuration:**
|
|
31
33
|
|
|
@@ -36,7 +38,7 @@ The default mode. Connects to a c15t backend for full consent lifecycle manageme
|
|
|
36
38
|
* The backend stays the source of truth for policy, translations, and jurisdiction logic
|
|
37
39
|
* Consent decisions can be stored beyond the current browser session for audit and support workflows
|
|
38
40
|
* Server-side systems can preload consent-aware behavior instead of waiting for client-only storage
|
|
39
|
-
* If the backend is temporarily unavailable, c15t
|
|
41
|
+
* If the backend is temporarily unavailable, c15t falls back locally and re-syncs rejections later. Grants made during the outage wait for fresh confirmation.
|
|
40
42
|
|
|
41
43
|
**Best for:** Production apps that need geolocation-based jurisdiction detection, consent record storage, and compliance audit trails.
|
|
42
44
|
|
|
@@ -9,7 +9,7 @@ c15t supports four consent models that control how consent defaults, banner visi
|
|
|
9
9
|
|--|--|--|--|
|
|
10
10
|
|`opt-in`|Explicit consent required|Blocking banner|`false` (except `necessary`)|
|
|
11
11
|
|`opt-out`|Processing allowed by default|Non-blocking notice|`true` (all granted)|
|
|
12
|
-
|`iab`|IAB TCF 2.
|
|
12
|
+
|`iab`|IAB TCF 2.4 for programmatic ads|TCF banner|Managed by TCF framework|
|
|
13
13
|
|`null`|No regulation detected|No banner|`true` (all auto-granted)|
|
|
14
14
|
|
|
15
15
|
There are two ways c15t determines which model applies:
|
|
@@ -39,7 +39,7 @@ When the policy has `consent.gpc: true`, the browser's Global Privacy Control si
|
|
|
39
39
|
|
|
40
40
|
### IAB
|
|
41
41
|
|
|
42
|
-
IAB Transparency and Consent Framework (TCF) 2.
|
|
42
|
+
IAB Transparency and Consent Framework (TCF) 2.4 mode for programmatic advertising compliance. Only activates when two conditions are met: the jurisdiction is `GDPR` or `UK_GDPR`, and `iab.enabled` is `true` in your configuration.
|
|
43
43
|
|
|
44
44
|
When active, IAB mode generates TC strings, registers the `__tcfapi` CMP API, and works with the Global Vendor List (GVL) for machine-readable consent signals. If `iab.enabled` is not set, GDPR jurisdictions fall back to standard opt-in.
|
|
45
45
|
|
|
@@ -14,6 +14,10 @@ There are four sources of cookies on a typical website:
|
|
|
14
14
|
**1. c15t itself**
|
|
15
15
|
c15t stores the user's consent state in a `c15t` cookie (and mirrors it to localStorage). This cookie records which categories the user consented to, when they consented, and their subject ID. This is a strictly necessary cookie - it exists so the site remembers the user's consent choice.
|
|
16
16
|
|
|
17
|
+
If reading `window.localStorage` or calling a storage method throws, c15t logs a warning and skips unavailable pending-submission queues. Runtime construction still completes, and cookie-backed choices can still be read and saved. In v2, a failed localStorage access check skips policy initialization and hides the banner. Optional categories without a saved choice remain denied.
|
|
18
|
+
|
|
19
|
+
If the browser also blocks cookies, it cannot retain your choice across page loads. Do not use `enabled: false` to handle storage restrictions: that option grants every category.
|
|
20
|
+
|
|
17
21
|
**2. Third-party scripts**
|
|
18
22
|
When you load a tracking script (Google Analytics, Meta Pixel, etc.), that script sets its own cookies. Google Analytics creates `_ga` and `_gid` cookies. Meta creates `_fbp` and `_fbc`. These cookies are set by the script's code running in the browser - c15t prevents them by not loading the script until the user consents.
|
|
19
23
|
|
|
@@ -20,5 +20,5 @@ group: reference
|
|
|
20
20
|
|--|--|
|
|
21
21
|
|`jurisdiction`|A privacy regulation code (e.g., `GDPR`, `CCPA`, `PIPEDA`, `QC_LAW25`) detected from the user's geolocation. Maps to a consent model. See [Consent Models](/docs/frameworks/react/concepts/consent-models) for the full mapping table.|
|
|
22
22
|
|GPC|Global Privacy Control — a browser signal (`Sec-GPC: 1`) indicating the user opts out of data sale/sharing. Honored when a policy sets `consent.gpc: true` (enabled by default in California presets).|
|
|
23
|
-
|TCF|IAB Transparency and Consent Framework (v2.
|
|
23
|
+
|TCF|IAB Transparency and Consent Framework (v2.4) — a standard for programmatic advertising consent in GDPR jurisdictions. See [IAB TCF](/docs/frameworks/react/iab/overview).|
|
|
24
24
|
|GVL|Global Vendor List — an IAB-maintained registry of ad-tech vendors and their declared purposes, used by the TCF consent flow.|
|
|
@@ -69,9 +69,9 @@ sequenceDiagram
|
|
|
69
69
|
|
|
70
70
|
**Init** — The store fetches the resolved policy, location, and translation data. In hosted mode this calls `GET /init` on your backend; in offline mode it resolves from `offlinePolicy.policyPacks` locally. If SSR data was passed to the provider, the network fetch is skipped entirely. See [Server-Side Utilities](/docs/frameworks/react/server-side) for SSR setup.
|
|
71
71
|
|
|
72
|
-
**Policy resolution** — When [policy packs](/docs/frameworks/react/concepts/policy-packs) are configured, the backend resolves the right policy for the visitor based on their geo-location (region → country → fallback → default). The resolved policy determines the consent model (`opt-in`, `opt-out`, `iab`, or `none`), which categories are in scope, and what UI to show. For `opt-out` and `none` models,
|
|
72
|
+
**Policy resolution** — When [policy packs](/docs/frameworks/react/concepts/policy-packs) are configured, the backend resolves the right policy for the visitor based on their geo-location (region → country → fallback → default). The resolved policy determines the consent model (`opt-in`, `opt-out`, `iab`, or `none`), which categories are in scope, and what UI to show. For `opt-out` and `none` models, categories are auto-granted only when there is no saved choice — unless the resolved policy has `consent.gpc: true` and the browser sends a Global Privacy Control signal, in which case `marketing` and `measurement` are denied. If no policy pack is configured, the legacy jurisdiction-to-model mapping is used instead. See [Consent Models](/docs/frameworks/react/concepts/consent-models) for details.
|
|
73
73
|
|
|
74
|
-
**Re-prompting** — If the resolved policy's material fingerprint differs from the fingerprint stored with the user's last consent, the banner
|
|
74
|
+
**Re-prompting** — If the resolved policy's material fingerprint differs from the fingerprint stored with the user's last consent, previous grants become invalid, while the saved choice and its denials remain. The consent UI opens again when the resolved policy has one; under a no-banner policy, invalidated grants stay denied until the user makes a new choice. This happens automatically when you change consent-affecting fields (model, categories, scope mode, allowed actions). Presentation-only changes do not trigger re-prompts. See [Policy Packs — Re-Prompting](/docs/frameworks/react/concepts/policy-packs#re-prompting) for details.
|
|
75
75
|
|
|
76
76
|
**Save** — When the user interacts with the banner or dialog, their choices are persisted to localStorage/cookies and synced to the backend (along with the `policySnapshotToken` if snapshot signing is configured). Script, iframe, and network gating rules update immediately based on the new consent state. See the [Script Loader](/docs/frameworks/react/script-loader), [Iframe Blocking](/docs/frameworks/react/iframe-blocking), and [Network Blocker](/docs/frameworks/react/network-blocker) guides for gating details.
|
|
77
77
|
|
|
@@ -85,7 +85,7 @@ The banner appears when any of these conditions are true:
|
|
|
85
85
|
2. **Policy changed** — the material policy fingerprint differs from the fingerprint stored with the user's last consent (re-prompting)
|
|
86
86
|
3. **Storage is accessible** — the browser allows localStorage (not blocked in private mode)
|
|
87
87
|
|
|
88
|
-
If the resolved model is `none` or `opt-out` (and `ui.mode` is `none`), consents are auto-granted and the banner never appears. See [Consent Models](/docs/frameworks/react/concepts/consent-models) and [Policy Packs](/docs/frameworks/react/concepts/policy-packs) for details.
|
|
88
|
+
If the resolved model is `none` or `opt-out` (and `ui.mode` is `none`) and no saved choice exists, consents are auto-granted and the banner never appears. An existing saved choice keeps its denials, but grants invalidated by a policy change stay denied until the user makes a new choice. See [Consent Models](/docs/frameworks/react/concepts/consent-models) and [Policy Packs](/docs/frameworks/react/concepts/policy-packs) for details.
|
|
89
89
|
|
|
90
90
|
## Debugging the Lifecycle
|
|
91
91
|
|
|
@@ -32,7 +32,7 @@ const policies = [
|
|
|
32
32
|
|Preset|Model|UI|Matches|
|
|
33
33
|
|--|--|--|--|
|
|
34
34
|
|`europeOptIn()`|`opt-in`|banner|EEA + UK countries + geo fallback|
|
|
35
|
-
|`europeIab()`|`iab`|banner|EEA + UK countries + geo fallback (TCF 2.
|
|
35
|
+
|`europeIab()`|`iab`|banner|EEA + UK countries + geo fallback (TCF 2.4)|
|
|
36
36
|
|`californiaOptOut()`|`opt-out`|none|US-CA region|
|
|
37
37
|
|`quebecOptIn()`|`opt-in`|banner|CA-QC region|
|
|
38
38
|
|`worldNoBanner()`|`none`|none|default fallback|
|
|
@@ -65,7 +65,7 @@ Each policy combination produces a different consent experience:
|
|
|
65
65
|
|`model: 'opt-out'`, `ui.mode: 'none'`|No banner, everything loads immediately — user opts out via a "Do Not Sell" link|
|
|
66
66
|
|`model: 'none'`, `ui.mode: 'none'`|No banner, all categories auto-granted silently|
|
|
67
67
|
|`model: 'opt-in'`, `ui.mode: 'dialog'`|Full-screen dialog, nothing loads until the user consents|
|
|
68
|
-
|`model: 'iab'`, `ui.mode: 'banner'`|IAB TCF 2.
|
|
68
|
+
|`model: 'iab'`, `ui.mode: 'banner'`|IAB TCF 2.4 banner with vendor-level controls|
|
|
69
69
|
|
|
70
70
|
## How Policy Resolution Works
|
|
71
71
|
|
|
@@ -176,6 +176,8 @@ back to the `eu` profile's `fallbackLanguage`.
|
|
|
176
176
|
|
|
177
177
|
When you change a policy in a way that affects consent semantics — like adding a category, changing the consent model, or modifying allowed actions — c15t automatically re-prompts returning users.
|
|
178
178
|
|
|
179
|
+
A material policy change invalidates previous grants but keeps the user's choice record and denials. Optional categories stay denied until the user makes a new choice. The resolved policy controls whether a banner or dialog appears; a no-banner policy does not silently grant previously rejected categories. A temporary hosted initialization failure does not count as an authoritative policy change.
|
|
180
|
+
|
|
179
181
|
This works through the **material policy fingerprint**: a hash of only the consent-affecting fields (model, categories, scope, allowed actions, grouped action layout, direction, proof settings). Presentation-only changes like copy, button styling, or scroll lock do not trigger re-prompts.
|
|
180
182
|
|
|
181
183
|
|Change|Re-prompts?|
|
|
@@ -51,6 +51,8 @@ For iframes outside of React (e.g., CMS content, server-rendered HTML), add `dat
|
|
|
51
51
|
|
|
52
52
|
When consent for the specified category is granted, c15t automatically swaps `data-src` to `src`, loading the iframe. When consent is revoked, `src` is moved back to `data-src`.
|
|
53
53
|
|
|
54
|
+
The `data-category` value must be one of `necessary`, `functionality`, `measurement`, `experience`, or `marketing`. An iframe with any other value, including an empty one, stays blocked, and c15t logs a warning to the console.
|
|
55
|
+
|
|
54
56
|
### Dynamic Iframes
|
|
55
57
|
|
|
56
58
|
c15t uses a `MutationObserver` to watch for dynamically added iframes. Any iframe with `data-category` added to the DOM after initialization is automatically processed.
|
|
@@ -72,7 +72,7 @@ Available presets:
|
|
|
72
72
|
|Preset|Model|Matches|
|
|
73
73
|
|--|--|--|
|
|
74
74
|
|`europeOptIn()`|`opt-in`|EEA + UK countries + geo fallback|
|
|
75
|
-
|`europeIab()`|`iab`|EEA + UK countries + geo fallback (TCF 2.
|
|
75
|
+
|`europeIab()`|`iab`|EEA + UK countries + geo fallback (TCF 2.4)|
|
|
76
76
|
|`californiaOptOut()`|`opt-out`|US-CA region|
|
|
77
77
|
|`quebecOptIn()`|`opt-in`|CA-QC region|
|
|
78
78
|
|`worldNoBanner()`|`none`|default fallback|
|
|
@@ -70,5 +70,5 @@ type Model = 'opt-in' | 'opt-out' | 'iab' | null;
|
|
|
70
70
|
|
|
71
71
|
* `'opt-in'` — Explicit consent required before tracking (GDPR)
|
|
72
72
|
* `'opt-out'` — Tracking allowed by default, user can opt out (CCPA)
|
|
73
|
-
* `'iab'` — IAB TCF 2.
|
|
73
|
+
* `'iab'` — IAB TCF 2.4 compliance mode
|
|
74
74
|
* `null` — No jurisdiction detected yet
|
|
@@ -6,7 +6,7 @@ group: reference
|
|
|
6
6
|
> ℹ️ **Info:**
|
|
7
7
|
> c15t's IAB TCF support can be used in production. Use Inth for a hosted, IAB TCF-certified CMP setup with a managed CMP ID, or register your own CMP with IAB Europe and configure your own CMP ID.
|
|
8
8
|
|
|
9
|
-
`IABConsentBanner` is a pre-built consent banner that follows the [IAB Transparency & Consent Framework (TCF) 2.
|
|
9
|
+
`IABConsentBanner` is a pre-built consent banner that follows the [IAB Transparency & Consent Framework (TCF) 2.4](https://iabeurope.eu/tcf-2-0/) specification. It renders when the consent model is set to `'iab'` and includes required disclosures like partner count, purpose summaries, and legitimate interest notices.
|
|
10
10
|
|
|
11
11
|
Use this component instead of `ConsentBanner` when you need IAB TCF compliance for programmatic advertising in EU jurisdictions.
|
|
12
12
|
|
|
@@ -6,7 +6,7 @@ group: reference
|
|
|
6
6
|
> ℹ️ **Info:**
|
|
7
7
|
> c15t's IAB TCF support can be used in production. Use Inth for a hosted, IAB TCF-certified CMP setup with a managed CMP ID, or register your own CMP with IAB Europe and configure your own CMP ID.
|
|
8
8
|
|
|
9
|
-
`IABConsentDialog` is an IAB TCF 2.
|
|
9
|
+
`IABConsentDialog` is an IAB TCF 2.4 compliant consent dialog that provides a tabbed interface for managing purpose consent and vendor preferences. It includes purpose grouping via stacks, individual purpose/vendor toggles, special purpose and feature disclosures, and legitimate interest handling.
|
|
10
10
|
|
|
11
11
|
## Tabs
|
|
12
12
|
|
|
@@ -19,7 +19,8 @@ Displays all IAB purposes grouped into:
|
|
|
19
19
|
* **Standalone purposes** — Purpose 1 (Store and/or access information on a device) is always shown standalone per IAB TCF spec
|
|
20
20
|
* **Stacks** — Groups of related purposes determined by the GVL. Each stack is expandable to show individual purpose toggles
|
|
21
21
|
* **Special features** — Opt-in features like precise geolocation
|
|
22
|
-
* **Essential functions** — Special purposes
|
|
22
|
+
* **Essential functions** — Special purposes, which are locked (no user toggle) because vendors need them for basic operation
|
|
23
|
+
* **Features** — An informational section, shown when at least one vendor declares a feature
|
|
23
24
|
|
|
24
25
|
Each purpose shows:
|
|
25
26
|
|
|
@@ -29,6 +30,14 @@ Each purpose shows:
|
|
|
29
30
|
* Legitimate interest toggle where applicable
|
|
30
31
|
* Expandable vendor list
|
|
31
32
|
|
|
33
|
+
### Features section
|
|
34
|
+
|
|
35
|
+
TCF Policies v5.0.b treat features as information, not choices. The Features section follows those rules:
|
|
36
|
+
|
|
37
|
+
* Under the heading, the dialog shows the IAB standard text from the vendor list (`standardTexts.features`). If the vendor list does not include it, the dialog shows the `iab.preferenceCenter.features.description` translation.
|
|
38
|
+
* Each feature shows its name, description, IAB illustrations under **Examples**, and the names of the vendors that use it.
|
|
39
|
+
* The section has no switches, lock icons or per-vendor toggles. Features only serve the purposes the user chooses elsewhere in the dialog.
|
|
40
|
+
|
|
32
41
|
### Vendors Tab
|
|
33
42
|
|
|
34
43
|
Displays all vendors from the GVL plus any custom vendors:
|
|
@@ -5,7 +5,7 @@ group: reference
|
|
|
5
5
|
---
|
|
6
6
|
## What is IAB TCF?
|
|
7
7
|
|
|
8
|
-
The [IAB Transparency & Consent Framework (TCF)](https://iabeurope.eu/tcf-2-0/) is a standardized protocol for communicating user consent choices to ad tech vendors in the programmatic advertising ecosystem. TCF 2.
|
|
8
|
+
The [IAB Transparency & Consent Framework (TCF)](https://iabeurope.eu/tcf-2-0/) is a standardized protocol for communicating user consent choices to ad tech vendors in the programmatic advertising ecosystem. TCF 2.4 is the current version and is widely adopted across the EU/EEA.
|
|
9
9
|
|
|
10
10
|
When your site participates in the IAB ecosystem (ad exchanges, SSPs, DSPs, DMPs), you need to:
|
|
11
11
|
|
|
@@ -25,7 +25,7 @@ If you self-host the c15t backend or want to operate as your own CMP, register y
|
|
|
25
25
|
|
|
26
26
|
## How c15t Implements TCF
|
|
27
27
|
|
|
28
|
-
c15t provides a complete IAB TCF 2.
|
|
28
|
+
c15t provides a complete IAB TCF 2.4 CMP (Consent Management Platform) implementation:
|
|
29
29
|
|
|
30
30
|
1. **Global Vendor List (GVL)** — Automatically fetched from the c15t backend. Contains the official IAB vendor registry with purposes, features, and stacks.
|
|
31
31
|
2. **IABConsentBanner** — A pre-built banner showing partner count, purpose summaries, and legitimate interest notices.
|
|
@@ -57,7 +57,11 @@ Purposes are grouped into **stacks** by the GVL for a simplified UI presentation
|
|
|
57
57
|
|
|
58
58
|
### Special Features
|
|
59
59
|
|
|
60
|
-
Features like precise geolocation or
|
|
60
|
+
Features like precise geolocation, or identifying devices based on information actively requested, that require explicit opt-in beyond standard consent.
|
|
61
|
+
|
|
62
|
+
### Features
|
|
63
|
+
|
|
64
|
+
Features, such as linking different devices, are means of processing that vendors use only for the purposes the user chooses. They have no consent toggle. The preference center lists them in an informational section with the IAB standard text.
|
|
61
65
|
|
|
62
66
|
### Legitimate Interest
|
|
63
67
|
|
|
@@ -67,11 +71,19 @@ Some purposes can be processed under legitimate interest rather than consent. Us
|
|
|
67
71
|
|
|
68
72
|
Each vendor in the GVL declares which purposes it uses, whether via consent or legitimate interest. The preference center lets users toggle consent per-vendor.
|
|
69
73
|
|
|
74
|
+
## Consent Scope
|
|
75
|
+
|
|
76
|
+
c15t stores TCF choices per site and browser, so every TC string it creates is service-specific. c15t does not sync TCF choices across devices.
|
|
77
|
+
|
|
78
|
+
Do not use `/consent/check` or your own account data to skip the TCF UI on another device. Reusing choices across devices is a multi-device scope, and the TCF requires you to disclose that scope in the first layer of the UI, which the prebuilt IAB components do not do.
|
|
79
|
+
|
|
80
|
+
The `isServiceSpecific` option is deprecated. TCF requires IsServiceSpecific=1 in every TC string, so c15t always encodes `true` and logs a warning if you pass `false`.
|
|
81
|
+
|
|
70
82
|
## Standard vs IAB Components
|
|
71
83
|
|
|
72
84
|
|Feature|ConsentBanner / ConsentDialog|IABConsentBanner / IABConsentDialog|
|
|
73
85
|
|--|--|--|
|
|
74
|
-
|Consent model|opt-in / opt-out|IAB TCF 2.
|
|
86
|
+
|Consent model|opt-in / opt-out|IAB TCF 2.4|
|
|
75
87
|
|Granularity|Category-level (measurement, marketing, etc.)|Purpose-level + vendor-level|
|
|
76
88
|
|Vendor management|No|Yes (full GVL integration)|
|
|
77
89
|
|TC String|No|Yes|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c15t/react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "React cookie banner, consent manager, preference center, and headless CMP components with IAB TCF and Consent Mode support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -163,16 +163,16 @@
|
|
|
163
163
|
"not op_mini all"
|
|
164
164
|
],
|
|
165
165
|
"dependencies": {
|
|
166
|
-
"@c15t/ui": "2.2.
|
|
166
|
+
"@c15t/ui": "2.2.2",
|
|
167
167
|
"@types/google.maps": "3.65.3",
|
|
168
|
-
"c15t": "2.
|
|
168
|
+
"c15t": "2.3.0"
|
|
169
169
|
},
|
|
170
170
|
"devDependencies": {
|
|
171
|
-
"@c15t/backend": "2.
|
|
172
|
-
"@c15t/iab": "2.
|
|
171
|
+
"@c15t/backend": "2.3.0",
|
|
172
|
+
"@c15t/iab": "2.3.0",
|
|
173
173
|
"@c15t/typescript-config": "0.0.1",
|
|
174
174
|
"@c15t/vitest-config": "1.0.0",
|
|
175
|
-
"@iabtechlabtcf/core": "^1.5.
|
|
175
|
+
"@iabtechlabtcf/core": "^1.5.22",
|
|
176
176
|
"genversion": "3.2.0",
|
|
177
177
|
"msw": "^2.14.6"
|
|
178
178
|
},
|
package/readme.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"Headless hooks for custom consent flows",
|
|
9
9
|
"Minimal configuration with TypeScript-first design",
|
|
10
10
|
"Comprehensive Consent Management Platform (CMP)",
|
|
11
|
-
"IAB TCF 2.
|
|
11
|
+
"IAB TCF 2.4 UI and hooks through the @c15t/react/iab subpath",
|
|
12
12
|
"Google Tag Manager, Google Consent Mode v2, Meta Pixel, and analytics integrations through @c15t/scripts",
|
|
13
13
|
"Built-in internationalization support",
|
|
14
14
|
"Seamless consent storage and tracking"
|