@c15t/react 3.1.0 → 3.2.0-canary.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.
Files changed (120) hide show
  1. package/AGENTS.md +11 -4
  2. package/README.md +2 -2
  3. package/dist/component-hooks/use-consent-gate.js +2 -0
  4. package/dist/component-hooks/use-consent-labels.js +2 -0
  5. package/dist/component-hooks/use-consent-text-direction.js +2 -0
  6. package/dist/component-hooks/use-vendors-listed-under.js +2 -0
  7. package/dist/components/consent-gate/atoms.js +1 -1
  8. package/dist/components/consent-gate/consent-gate.js +1 -1
  9. package/dist/components/iab-prompt/atoms/root.js +1 -1
  10. package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
  11. package/dist/components/preferences/atoms/button.js +1 -1
  12. package/dist/components/preferences/atoms/vendor-list.js +1 -1
  13. package/dist/components/prompt/atoms/root.js +1 -1
  14. package/dist/components/prompt/components.js +1 -1
  15. package/dist/components/prompt/policy-actions.js +1 -1
  16. package/dist/components/prompt/prompt.js +1 -1
  17. package/dist/components/prompt/surface-context.js +1 -1
  18. package/dist/components/prompt/use-banner-copy.js +1 -1
  19. package/dist/components/shared/streamed-surface.js +1 -1
  20. package/dist/headless.js +1 -1
  21. package/dist/index-parts/offline.js +1 -1
  22. package/dist/modes.js +1 -1
  23. package/dist/transports/offline.js +1 -1
  24. package/dist/version.js +1 -1
  25. package/dist-types/component-hooks/use-consent-gate.d.ts +102 -0
  26. package/dist-types/component-hooks/use-consent-labels.d.ts +32 -0
  27. package/dist-types/component-hooks/use-consent-text-direction.d.ts +22 -0
  28. package/dist-types/component-hooks/use-vendors-listed-under.d.ts +30 -0
  29. package/dist-types/components/prompt/atoms/root.d.ts +3 -3
  30. package/dist-types/components/prompt/prompt.d.ts +4 -3
  31. package/dist-types/components/prompt/surface-context.d.ts +1 -14
  32. package/dist-types/components/prompt/use-banner-copy.d.ts +8 -3
  33. package/dist-types/components/shared/streamed-surface.d.ts +47 -10
  34. package/dist-types/headless.d.ts +10 -1
  35. package/dist-types/hooks/use-iab-policy-guard.d.ts +17 -1
  36. package/dist-types/hooks/use-scroll-lock.d.ts +16 -2
  37. package/dist-types/index-parts/offline.d.ts +1 -1
  38. package/dist-types/modes.d.ts +1 -1
  39. package/dist-types/module-hooks/script-loader.d.ts +9 -0
  40. package/dist-types/provider.d.ts +3 -3
  41. package/dist-types/transports/offline.d.ts +2 -2
  42. package/dist-types/version.d.ts +1 -1
  43. package/docs/README.md +11 -4
  44. package/docs/concepts/choose-your-setup.md +9 -9
  45. package/docs/concepts/consent-state.md +81 -9
  46. package/docs/concepts/data-fetching.md +30 -23
  47. package/docs/concepts/modes.md +85 -35
  48. package/docs/customization/recipes.md +4 -4
  49. package/docs/customization/stylesheets.md +3 -3
  50. package/docs/customization/tailwind.md +17 -34
  51. package/docs/customization/translations.md +18 -12
  52. package/docs/frameworks/react/clear-on-revocation.md +3 -2
  53. package/docs/frameworks/react/components/consent-banner.md +3 -3
  54. package/docs/frameworks/react/components/consent-provider.md +29 -29
  55. package/docs/frameworks/react/gpp.md +1 -1
  56. package/docs/frameworks/react/headless.md +146 -19
  57. package/docs/frameworks/react/iab.md +8 -1
  58. package/docs/frameworks/react/quickstart.md +1 -1
  59. package/docs/frameworks/react/rendering.md +1 -1
  60. package/docs/frameworks/react/translations.md +9 -9
  61. package/docs/frameworks/react/troubleshooting.md +1 -1
  62. package/docs/frameworks/react/upgrade-v3.md +39 -32
  63. package/docs/frameworks/react/vendor-consent.md +5 -0
  64. package/docs/guides/banner-experiments.md +3 -3
  65. package/docs/guides/troubleshooting.md +3 -2
  66. package/docs/integrations/adobe-analytics.md +2 -2
  67. package/docs/integrations/ahrefs-analytics.md +2 -2
  68. package/docs/integrations/amplitude.md +2 -2
  69. package/docs/integrations/clay.md +305 -0
  70. package/docs/integrations/clearbit.md +2 -2
  71. package/docs/integrations/cloudflare-web-analytics.md +2 -2
  72. package/docs/integrations/cloudflare-zaraz.md +2 -2
  73. package/docs/integrations/crisp.md +2 -2
  74. package/docs/integrations/databuddy.md +2 -2
  75. package/docs/integrations/fathom-analytics.md +2 -2
  76. package/docs/integrations/front-chat.md +2 -2
  77. package/docs/integrations/google-maps.md +2 -2
  78. package/docs/integrations/google-tag-manager.md +59 -2
  79. package/docs/integrations/google-tag.md +2 -2
  80. package/docs/integrations/heap.md +2 -2
  81. package/docs/integrations/hightouch.md +2 -2
  82. package/docs/integrations/hotjar.md +40 -26
  83. package/docs/integrations/intercom.md +2 -2
  84. package/docs/integrations/klaviyo.md +2 -2
  85. package/docs/integrations/linkedin-insights.md +2 -2
  86. package/docs/integrations/logrocket.md +2 -2
  87. package/docs/integrations/matomo-analytics.md +2 -2
  88. package/docs/integrations/meta-pixel.md +2 -2
  89. package/docs/integrations/microsoft-clarity.md +2 -2
  90. package/docs/integrations/microsoft-uet.md +2 -2
  91. package/docs/integrations/mixpanel-analytics.md +2 -2
  92. package/docs/integrations/one-dollar-stats.md +2 -2
  93. package/docs/integrations/openai-pixel.md +2 -2
  94. package/docs/integrations/other-ads-and-pixels.md +94 -0
  95. package/docs/integrations/other-analytics.md +108 -0
  96. package/docs/integrations/other-chat-and-support.md +112 -0
  97. package/docs/integrations/other-email-and-sms.md +156 -0
  98. package/docs/integrations/other-experience.md +222 -0
  99. package/docs/integrations/overview.md +55 -21
  100. package/docs/integrations/pinterest-tag.md +2 -2
  101. package/docs/integrations/pirsch.md +2 -2
  102. package/docs/integrations/plausible-analytics.md +2 -2
  103. package/docs/integrations/posthog.md +2 -2
  104. package/docs/integrations/promptwatch.md +2 -2
  105. package/docs/integrations/reddit-pixel.md +2 -2
  106. package/docs/integrations/rudderstack.md +2 -2
  107. package/docs/integrations/rybbit-analytics.md +2 -2
  108. package/docs/integrations/segment.md +2 -2
  109. package/docs/integrations/sentry.md +2 -2
  110. package/docs/integrations/shopify.md +323 -0
  111. package/docs/integrations/snapchat-pixel.md +2 -2
  112. package/docs/integrations/tiktok-pixel.md +2 -2
  113. package/docs/integrations/umami-analytics.md +2 -2
  114. package/docs/integrations/vercel-analytics.md +2 -2
  115. package/docs/integrations/vercel-speed-insights.md +7 -2
  116. package/docs/integrations/x-pixel.md +2 -2
  117. package/docs/integrations/youtube.md +2 -2
  118. package/package.json +10 -32
  119. package/dist/components/prompt/resolve-banner-primary-actions.js +0 -1
  120. package/dist-types/components/prompt/resolve-banner-primary-actions.d.ts +0 -14
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Headless
3
- description: Build a custom consent banner in React with the c15t/react/headless
4
- hooks inside your ConsentProvider.
3
+ description: Build a custom consent banner or embed gate in React with the
4
+ c15t/react/headless hooks inside your ConsentProvider.
5
5
  group: frameworks
6
6
  ---
7
7
 
@@ -30,29 +30,28 @@ The headless hooks read the runtime from the nearest `ConsentProvider` from
30
30
  banner, and keep the dialog and persistent preferences control.
31
31
 
32
32
  ```tsx title="src/consent-banner.tsx"
33
- import { useHeadlessConsentUI, useTranslations } from 'c15t/react/headless';
34
-
35
- const ACTION_LABELS = {
36
- accept: 'acceptAll',
37
- customize: 'customize',
38
- dismiss: 'acknowledge',
39
- reject: 'rejectAll',
40
- save: 'save',
41
- } as const;
33
+ import {
34
+ useBannerCopy,
35
+ useConsentLabels,
36
+ useHeadlessConsentUI,
37
+ } from 'c15t/react/headless';
42
38
 
43
39
  export const Banner = () => {
44
40
  const { banner, performAction, openDialog } = useHeadlessConsentUI();
45
- const { common, rights } = useTranslations();
41
+ const { title, description } = useBannerCopy();
42
+ const labels = useConsentLabels();
46
43
 
47
44
  if (!banner.isVisible) {
48
45
  return null;
49
46
  }
50
47
 
51
48
  return (
52
- <section aria-label="Privacy">
49
+ <section aria-label={title}>
50
+ <h2>{title}</h2>
51
+ <p>{description}</p>
53
52
  {banner.preferenceControls.map((right) => (
54
53
  <button key={right} type="button" onClick={openDialog}>
55
- {right === 'opt-out' ? rights?.optOut : rights?.preferences}
54
+ {labels.preferenceControls[right]}
56
55
  </button>
57
56
  ))}
58
57
  {banner.actionGroups.map((group) => (
@@ -64,7 +63,7 @@ export const Banner = () => {
64
63
  data-primary={banner.primaryActions.includes(action) || undefined}
65
64
  onClick={() => performAction(action)}
66
65
  >
67
- {common[ACTION_LABELS[action]]}
66
+ {labels.actions[action]}
68
67
  </button>
69
68
  ))}
70
69
  </div>
@@ -76,8 +75,10 @@ export const Banner = () => {
76
75
 
77
76
  `banner.actionGroups` is the resolved layout: reject and accept share a group
78
77
  at equal prominence, and the rest follow. `banner.orderedActions` is the same
79
- list flattened. Under a notice the only action is `dismiss`, and
80
- `banner.preferenceControls` recommends additional buttons for opening
78
+ list flattened. `banner.primaryActions` lists the actions to style as
79
+ primary. Under a notice the only action is `dismiss`, and
80
+ `banner.primaryActions` contains it. `banner.preferenceControls` recommends
81
+ additional buttons for opening
81
82
  preferences. Under a notice it contains `opt-out`, which selects the
82
83
  "Do not sell or share my data" label. The example renders that button and
83
84
  an "OK" button. Both controls keep their own command: opening
@@ -86,6 +87,20 @@ preferences and dismissing the notice.
86
87
  The list is a rendering helper. It does not establish that your UI implements
87
88
  all policy rights. Provide disclosure and persistent preferences access.
88
89
 
90
+ `useBannerCopy()` returns the `title` and `description` for the active
91
+ prompt. A notice reads `cookieBanner.noticeTitle` and
92
+ `cookieBanner.noticeDescription`, then falls back to the choice copy in the
93
+ same language before English. `useConsentLabels()` returns
94
+ `actions`, a label for each action, and `preferenceControls`, a label for
95
+ each preference control. `dismiss` reads `common.acknowledge`, then
96
+ `common.dismiss`. Both hooks follow the active language and fill missing
97
+ keys from English, and they are the same strings the pre-built banner
98
+ renders.
99
+
100
+ The hooks wrap `resolvePromptCopy`, `resolveActionLabels`, and
101
+ `resolvePreferenceControlLabels`. Those take a translations object and
102
+ are exported from `c15t`, so code outside React can produce the same copy.
103
+
89
104
  `performAction` saves all categories for `accept`, none for `reject`, the
90
105
  current draft for `save`, records a dismissal for `dismiss`, and opens the
91
106
  preference center for `customize`. `banner.diagnostics` reports when a host
@@ -94,5 +109,117 @@ prominence. Review each diagnostic when configuring custom presentation.
94
109
 
95
110
  `banner.variant`, `banner.position`, and `banner.blocking` carry the resolved
96
111
  shape from `presentation.prompt`, so a headless surface can follow the same
97
- bar, widget, or wall choice the pre-built banner would make, and can trap
98
- focus and lock scroll exactly when `blocking` is true.
112
+ bar, widget, or wall choice the pre-built banner would make. The pre-built
113
+ banner traps focus and locks page scroll while `blocking` is true. To do
114
+ the same, call `useFocusTrap(blocking, ref)` and `useScrollLock(blocking)`
115
+ from the headless entry, with `blocking` set to
116
+ `banner.isVisible && banner.blocking` and `ref` attached to your banner's
117
+ root element. Scroll locks are counted, so the page scrolls again once the
118
+ banner and any open dialog have both let go.
119
+
120
+ c15t can show consent copy in the visitor's browser language, which may run
121
+ right to left on a left-to-right page. `useConsentTextDirection()` returns
122
+ `'rtl'` or `'ltr'` for the language the copy is in. Set it as `dir` on your
123
+ banner and preference center roots, as the pre-built ones do.
124
+
125
+ The pre-built banner and dialog render nothing under the IAB model, and
126
+ the TCF surfaces inside `<IABProvider>` take over. A
127
+ custom surface that handles only opt-in and opt-out should do the same:
128
+ return `null` when `useModel()` is `'iab'`. Call `useIABPolicyGuard()` in
129
+ it as well. It throws `IABUnavailableError` while rendering when the
130
+ policy needs IAB and the app never loaded `<IABProvider>`, so the gap
131
+ shows up in your error boundary instead of as a missing banner.
132
+
133
+ ## Build a custom consent gate
134
+
135
+ `useConsentGate(category)` tells a custom gate what to render for one
136
+ consent category, with translated copy and an action that opens the
137
+ preference center. Use it when the stock `ConsentGate` placeholder does not
138
+ fit your design, for example a design system card in front of a video or
139
+ map embed. `ConsentGate` runs on the same hook, so both gates follow the
140
+ same rules.
141
+
142
+ This gate renders its children once `marketing` is granted and the page has
143
+ hydrated, the policy message when the policy blocks `marketing`, and
144
+ otherwise a placeholder with a button that opens the preference center:
145
+
146
+ ```tsx title="src/video-gate.tsx"
147
+ import { useConsentGate } from 'c15t/react/headless';
148
+ import type { ReactNode } from 'react';
149
+
150
+ export const VideoGate = ({ children }: { children: ReactNode }) => {
151
+ const { state, title, actionLabel, policyBlockedMessage, requestConsent } =
152
+ useConsentGate('marketing');
153
+
154
+ if (state === 'granted') {
155
+ return children;
156
+ }
157
+ if (state === 'pending') {
158
+ return null;
159
+ }
160
+ if (state === 'policy-blocked') {
161
+ return <p>{policyBlockedMessage}</p>;
162
+ }
163
+ return (
164
+ <div>
165
+ <p>{title}</p>
166
+ <button type="button" onClick={requestConsent}>
167
+ {actionLabel}
168
+ </button>
169
+ </div>
170
+ );
171
+ };
172
+ ```
173
+
174
+ Render `VideoGate` inside your `ConsentProvider` around the embed, such as a
175
+ YouTube `iframe`.
176
+
177
+ | Field | Type | Description |
178
+ | ---------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------- |
179
+ | `state` | `ConsentGateState` | `'granted'`, `'pending'`, `'denied'` or `'policy-blocked'`. |
180
+ | `categoryTitle` | `string` | `consentTypes.<category>.title` from your translations, or the category name when they have none. |
181
+ | `title` | `string` or `undefined` | `consentGate.title`, `Accept {category} consent to view this content.`, with `{category}` replaced by `categoryTitle`. |
182
+ | `actionLabel` | `string` or `undefined` | `consentGate.actionButton`, `Enable {category} consent`, with `{category}` replaced by `categoryTitle`. |
183
+ | `policyBlockedMessage` | `string` | `consentGate.policyBlocked`, or the English message when your translations have none. |
184
+ | `requestConsent` | `() => void` | Opens the preference center. |
185
+
186
+ The result type is exported as `UseConsentGateResult` and the state type as
187
+ `ConsentGateState`. Render from `state`:
188
+
189
+ * `'granted'`: the effective permission for the category is granted and
190
+ the page has hydrated. Render the content.
191
+ * `'pending'`: permission is granted but the page has not hydrated yet.
192
+ Render nothing.
193
+ * `'denied'`: permission is not granted. Render a placeholder with a
194
+ button that calls `requestConsent`.
195
+ * `'policy-blocked'`: the policy has `scopeMode: 'strict'` and its scope
196
+ leaves the category out, so the visitor cannot grant it. Render
197
+ `policyBlockedMessage` without a button. `necessary` is never blocked.
198
+
199
+ Permission is the value `useConsent(category)` returns. An opt-out policy
200
+ can grant it before the visitor chooses, and a privacy signal such as GPC
201
+ can withdraw a stored grant.
202
+
203
+ Granted content never appears in the server HTML. When the server snapshot
204
+ grants the category, `state` is `'pending'` in the server HTML and during
205
+ hydration, then turns `'granted'`, so an iframe inside a streamed
206
+ `Suspense` boundary loads once. Server rendering and hydration evaluate the
207
+ snapshot at its own evaluation time and never read the clock, so the gate
208
+ works in a statically prerendered page.
209
+
210
+ `requestConsent` does not grant the category, because the visitor still has
211
+ to save. The preference center opens on its usual view, not on the
212
+ category. Mount a `ConsentDialog` in the same provider so it has something
213
+ to open. The stock gate button starts loading the pre-built dialog's code
214
+ on hover, focus and idle time. `requestConsent` doesn't, so without a
215
+ banner or dialog trigger on the page, the first open waits for that
216
+ download.
217
+
218
+ When it mounts, the hook registers the category with the provider, as
219
+ `ConsentGate` does, so the preference center can list it. If you set
220
+ `options.consentCategories` on the provider, list the category there too,
221
+ so the preference center shows it before a gate mounts, including on pages
222
+ without one.
223
+
224
+ The hook renders no wrapper element. Reserve the embed's space in your own
225
+ markup so the page does not shift when the placeholder is replaced.
@@ -274,7 +274,7 @@ The backend's `additionalConsent` option sets the cache, which defaults to the
274
274
 
275
275
  ### List the providers in the browser
276
276
 
277
- Offline mode has no backend, and some backends do not list providers. In
277
+ Local mode has no backend, and some backends do not list providers. In
278
278
  those setups, pass `additionalConsent` with your other IAB options: the
279
279
  `IABProvider` props in React, Next.js and TanStack Start, the `createIAB`
280
280
  options in JavaScript, and the `iab` option of the Vue, Nuxt, Svelte, Astro
@@ -380,6 +380,13 @@ its own CMP ID. It lists the CMPs it has certified on its
380
380
  Ask Google to list your CMP ID through its CMP intake form before you rely on
381
381
  the AC string in production.
382
382
 
383
+ ## c15t's IAB CMP registration
384
+
385
+ Inth makes c15t. Inth's legal entity, Consent Management Inc., is registered
386
+ with IAB Europe as a CMP under CMP ID 490. The
387
+ [IAB Europe CMP list](https://iabeurope.eu/cmp-list/) shows it under the name
388
+ Consent Management Inc.
389
+
383
390
  ## How the preference centre shows features
384
391
 
385
392
  The stock IAB preference centres in React, Next.js, Vue, Svelte, Astro and the
@@ -25,7 +25,7 @@ You need an [Inth](https://inth.com) project. In the project, set the policy
25
25
  rules for your regions, add your site's origin to the trusted origins, and copy
26
26
  the backend URL.
27
27
  [Choose your setup](../../concepts/choose-your-setup.md#decide-who-runs-the-consent-backend)
28
- covers self-hosting and offline mode.
28
+ covers self-hosting and local mode.
29
29
 
30
30
  The guide uses `hosted()` mode. Policy, translation and vendor edits in your
31
31
  project reach visitors on their next page load, without a rebuild.
@@ -54,7 +54,7 @@ usually shows 50 to 250 ms sooner. There is nothing to configure.
54
54
 
55
55
  The early request can't read your app's options, so in a few setups it won't
56
56
  match and the app sends a second `/init`. Pass `initPrefetch: false` when the
57
- app uses `offline()` or gives `hosted()` its own `backendURL`, `fetch`,
57
+ app uses `local()` or gives `hosted()` its own `backendURL`, `fetch`,
58
58
  `initURL` or `headers`. With its own `fetch`, `hosted()` sends `/init` through
59
59
  it and ignores the early response. If it sets `credentials`, `overrides`, `storageKey`, an
60
60
  `experiment`, or `journey: false`, pass the same values as
@@ -12,10 +12,10 @@ The banner, dialog, widget and gate placeholder read their text from one set
12
12
  of translations for the visitor's language. Which set depends on the
13
13
  provider's `mode`:
14
14
 
15
- | Mode | Copy comes from |
16
- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
- | `hosted()` | The backend's `/init` response for the visitor's language, from the translations in your Inth project or your self-hosted backend's configuration, with your `i18n` messages for that language on top |
18
- | `offline()` | Your `i18n` messages, merged over bundled English |
15
+ | Mode | Copy comes from |
16
+ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | `hosted()` | The backend's `/init` response for the visitor's language, from the translations in your Inth project or your self-hosted backend's configuration, with your `i18n` messages for that language on top |
18
+ | `local()` | Your `i18n` messages, merged over bundled English |
19
19
 
20
20
  With a backend, edit wording in your Inth project first, so it changes without
21
21
  a deploy. Code can override that copy in two places:
@@ -35,12 +35,12 @@ When your project has no copy for it, the backend returns its fallback
35
35
  language. Set `overrides.language` in the provider options to request a
36
36
  specific language instead.
37
37
 
38
- In `offline()` mode, `i18n.locale` picks the language, `en` by default. The
38
+ In `local()` mode, `i18n.locale` picks the language, `en` by default. The
39
39
  browser's language is not read.
40
40
 
41
41
  The banner, dialog and widget set `dir` from the active language. Right-to-left
42
- languages such as Arabic and Hebrew render with `dir="rtl"`, and a floating
43
- banner you did not position yourself moves to the opposite side.
42
+ languages such as Arabic and Hebrew render with `dir="rtl"`. The banner stays
43
+ in the same corner.
44
44
 
45
45
  ## Change the copy in code
46
46
 
@@ -73,7 +73,7 @@ Pass them to the provider in the file that renders it, such as
73
73
 
74
74
  For the visitor's language, your messages replace the backend's copy key by
75
75
  key. Keys you leave out keep the project's wording, and messages for other
76
- languages are not applied. In `offline()` mode c15t bundles only English, so
76
+ languages are not applied. In `local()` mode c15t bundles only English, so
77
77
  keys you leave out for another language show in English.
78
78
 
79
79
  The provider reads `i18n` once, when it mounts. In development it logs a
@@ -137,7 +137,7 @@ Setting the language without calling `init()` changes nothing on screen. Your
137
137
  `i18n` messages for the new language apply on top of the copy that arrives.
138
138
  Render the switcher anywhere inside `ConsentProvider`.
139
139
 
140
- Switching needs a backend. In `offline()` mode the copy stays in the
140
+ Switching needs a backend. In `local()` mode the copy stays in the
141
141
  `i18n.locale` language.
142
142
 
143
143
  If your site changes language through its own i18n library, you can instead
@@ -44,7 +44,7 @@ is down, run the build with `C15T_ON_BUILD_ERROR=runtime`. `snapshot` is then
44
44
 
45
45
  Only a build that uses `manifest()` downloads the manifest. The plugin
46
46
  fetches after Vite has dropped the code the app does not use, so a `hosted()`
47
- or `offline()` build never contacts the backend and never fails this way.
47
+ or `local()` build never contacts the backend and never fails this way.
48
48
 
49
49
  An error or warning that says `no backend URL is set` means the plugin found
50
50
  no `backendURL` option and no variable. Set `VITE_C15T_BACKEND_URL` (or
@@ -18,7 +18,7 @@ Read ./upgrade-v3.md first and follow it in order. Do not guess v3 APIs from mem
18
18
  1. List every c15t dependency and import: `@c15t/react`, `@c15t/scripts`, `@c15t/dev-tools` and `c15t`.
19
19
  2. Check that React is 18 or later. Stop and tell me if it needs a major upgrade.
20
20
  3. Replace the packages with `c15t`, plus `@c15t/integrations` if the app uses vendor helpers.
21
- 4. Run `npx @c15t/cli codemods packages-to-c15t consent-provider-options root-exports-to-subpaths use-consent-manager-to-hooks scripts-to-integrations dev-tools-to-c15t policy-packs-to-policy-rules callbacks-to-v3 theme-to-consent-theme iab-option-to-iab-provider css-variables-to-v3 postcss-tailwind3 --dry-run --json`. Review the output, then run the same command without `--dry-run`. The codemods leave a `TODO(c15t v3)` comment wherever they need you to finish a change.
21
+ 4. Install the Inth CLI with `npm install -g @inth/cli` if `inth` is missing, then run `inth c15t codemods packages-to-c15t consent-provider-options root-exports-to-subpaths use-consent-manager-to-hooks scripts-to-integrations dev-tools-to-c15t policy-packs-to-policy-rules callbacks-to-v3 theme-to-consent-theme iab-option-to-iab-provider css-variables-to-v3 postcss-tailwind3 --dry-run --json`. Review the output, then run the same command without `--dry-run`. The codemods leave a `TODO(c15t v3)` comment wherever they need you to finish a change.
22
22
  5. Check that `ConsentManagerProvider` is now `ConsentProvider` from `c15t/react`, with a transport such as `hosted()` as `mode`. Finish any custom mode the codemod marked.
23
23
  6. Update callbacks, policy presets, renamed options and components, theme CSS, CSS variables and IAB setup as the guide describes.
24
24
  7. Keep the existing backend URL. If the app runs its own c15t backend, stop and tell me, because it has to be upgraded in the same release with <https://v3.c15t.com/docs/self-host/upgrade-v3.md>.
@@ -34,6 +34,8 @@ When you finish, list the files you changed, anything you could not migrate, and
34
34
  client can't read a v2 backend. See
35
35
  [Upgrade a self-hosted backend](https://c15t.com/docs/self-host/upgrade-v3).
36
36
  Inth-hosted backends need no change.
37
+ * Install the [Inth CLI](https://c15t.com/docs/cli/overview#install-the-inth-cli). The
38
+ codemods run through `inth c15t codemods`.
37
39
 
38
40
  ## Replace the packages
39
41
 
@@ -111,7 +113,7 @@ ends in v4; versions already published stay on npm.
111
113
  To preview the import changes in JavaScript and TypeScript files:
112
114
 
113
115
  ```bash
114
- npx @c15t/cli codemods scripts-to-integrations --dry-run --json
116
+ inth c15t codemods scripts-to-integrations --dry-run --json
115
117
  ```
116
118
 
117
119
  Review the output, then run it again without `--dry-run`. The codemod runs
@@ -126,7 +128,7 @@ that called it re-rendered on every change. v3 has one hook per field. Run the
126
128
  codemod first:
127
129
 
128
130
  ```bash
129
- npx @c15t/cli codemods use-consent-manager-to-hooks --dry-run --json
131
+ inth c15t codemods use-consent-manager-to-hooks --dry-run --json
130
132
  ```
131
133
 
132
134
  Review the proposed files, then run it again without `--dry-run`. For this v2
@@ -227,7 +229,7 @@ a transport as `mode`:
227
229
  | v2 option | v3 option |
228
230
  | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
229
231
  | `mode: 'hosted'` with `backendURL` | `mode: hosted({ backendURL })` |
230
- | `mode: 'offline'` with `offlinePolicy` | `mode: offline({ policyRules })` |
232
+ | `mode: 'offline'` with `offlinePolicy` | `mode: local({ policyRules })` |
231
233
  | `mode: 'custom'` with `endpointHandlers` | `mode: custom(transport)`, where `transport` implements the v3 transport interface. `endpointHandlers` throws. |
232
234
 
233
235
  ```tsx title="After (v3): the provider, changed lines"
@@ -239,12 +241,13 @@ import { ConsentBanner, ConsentDialog, ConsentProvider, hosted } from 'c15t/reac
239
241
  >
240
242
  ```
241
243
 
242
- The `consent-provider-options` codemod rewrites the hosted and offline modes
243
- and adds the transport import. It can't translate `endpointHandlers`, so it
244
- leaves a `TODO(c15t v3)` comment on a custom mode:
244
+ The `consent-provider-options` codemod rewrites the hosted mode and adds the
245
+ `hosted` import. It leaves offline and custom modes as they are, with a
246
+ `TODO(c15t v3)` comment. Move `offlinePolicy.policyPacks` to
247
+ `local({ policyRules })` by hand. It can't translate `endpointHandlers`:
245
248
 
246
249
  ```bash
247
- npx @c15t/cli codemods consent-provider-options --dry-run --json
250
+ inth c15t codemods consent-provider-options --dry-run --json
248
251
  ```
249
252
 
250
253
  Keep your backend URL. An existing hosted URL such as
@@ -260,7 +263,7 @@ which bundles the policy when Vite builds. It skips `/init` only when the
260
263
  policy has no location rules or the page knows the visitor's location. See
261
264
  [consent modes](../../concepts/modes.md).
262
265
 
263
- Offline mode keeps choices in the browser and records nothing on a server. Not
266
+ Local mode keeps choices in the browser and records nothing on a server. Not
264
267
  recommended for production environments.
265
268
 
266
269
  If your app renders on the server with React Router or Remix, the banner still
@@ -285,7 +288,7 @@ payload, and on `onConsentSet` and `onBannerFetched`, which it can't choose a
285
288
  replacement for:
286
289
 
287
290
  ```bash
288
- npx @c15t/cli codemods callbacks-to-v3 --dry-run --json
291
+ inth c15t codemods callbacks-to-v3 --dry-run --json
289
292
  ```
290
293
 
291
294
  v2's `onConsentChanged` fired only when a save changed at least one category's
@@ -329,12 +332,12 @@ backend:
329
332
  change a hosted policy.
330
333
  * **Self-hosted**: move the backend's `policyPacks` to `manifest.policyRules`.
331
334
  See [policy configuration](https://c15t.com/docs/self-host/guides/policy-packs).
332
- * **Offline**: move `offlinePolicy.policyPacks` to `offline({ policyRules })`.
335
+ * **No backend**: move `offlinePolicy.policyPacks` to `local({ policyRules })`.
333
336
 
334
- ```tsx title="After (v3): offline policy rules"
335
- import { offline, policyRulePresets } from 'c15t';
337
+ ```tsx title="After (v3): local policy rules"
338
+ import { local, policyRulePresets } from 'c15t';
336
339
 
337
- const mode = offline({
340
+ const mode = local({
338
341
  policyRules: [
339
342
  policyRulePresets.europeOptIn(),
340
343
  policyRulePresets.worldOptOutNoPrompt(),
@@ -342,7 +345,7 @@ const mode = offline({
342
345
  });
343
346
  ```
344
347
 
345
- In Next.js, import `offline` from `c15t/next` instead and set the result as
348
+ In Next.js, import `local` from `c15t/next` instead and set the result as
346
349
  `mode` in `c15t.config.ts`. Both take the same `policyRules`.
347
350
 
348
351
  | v2 preset | v3 preset |
@@ -357,13 +360,14 @@ rights, while `worldOptOutNoPrompt()` keeps v2's opt-out and preference rights.
357
360
 
358
361
  The `policy-packs-to-policy-rules` codemod renames `policyPackPresets` and
359
362
  `worldNoBanner()`, and moves the import to `c15t` where it came from
360
- `c15t/react` or `c15t/next`. `consent-provider-options` moves
361
- `offlinePolicy.policyPacks` into `offline({ policyRules })`. Preset calls need
363
+ `c15t/react` or `c15t/next`. `consent-provider-options` leaves offline mode
364
+ with a `TODO(c15t v3)` comment; move `offlinePolicy.policyPacks` into
365
+ `offline({ policyRules })` by hand. Preset calls need
362
366
  no other change, but a hand-written v2 pack uses `consent` and `ui` keys,
363
367
  which v3 rules replace with flat keys such as `model` and `prompt`; the
364
368
  codemods mark those with a `TODO(c15t v3)` comment.
365
369
 
366
- v3 adds presets for more regions. `offline()` without `policyRules` uses
370
+ v3 adds presets for more regions. `local()` without `policyRules` uses
367
371
  `recommendedPolicyRules()`. Read [policies](../../concepts/policies.md) before you
368
372
  change a rule's model or prompt, and
369
373
  [how consent works](../../concepts/how-consent-works.md) for the difference
@@ -392,13 +396,13 @@ between a permission and a recorded choice.
392
396
  | `iab: iab({ ... })` | `IABProvider`. See [Move IAB setup](#move-iab-setup-to-iabprovider). |
393
397
 
394
398
  The `consent-provider-options` codemod renames `ConsentManagerProvider` and
395
- its option and prop types, turns `mode` and `backendURL` into `hosted()` or
396
- `offline()`, and renames `iframeBlockerConfig`. v2 still accepted the
397
- deprecated `translations` option; the `translations-to-i18n` codemod moves it
398
- to `i18n`.
399
+ its option and prop types, turns `mode: 'hosted'` and `backendURL` into
400
+ `hosted()`, and renames `iframeBlockerConfig`. It leaves offline and custom
401
+ modes with a `TODO(c15t v3)` comment. v2 still accepted the deprecated
402
+ `translations` option; rename it to `i18n` by hand.
399
403
 
400
404
  ```bash
401
- npx @c15t/cli codemods consent-provider-options translations-to-i18n --dry-run --json
405
+ inth c15t codemods consent-provider-options --dry-run --json
402
406
  ```
403
407
 
404
408
  `ConsentBanner`, `ConsentDialog`, `ConsentDialogLink`, `ConsentDialogTrigger`
@@ -426,7 +430,7 @@ The `root-exports-to-subpaths` codemod moves these imports to their new
426
430
  entries and marks removed names with a `TODO(c15t v3)` comment:
427
431
 
428
432
  ```bash
429
- npx @c15t/cli codemods root-exports-to-subpaths --dry-run --json
433
+ inth c15t codemods root-exports-to-subpaths --dry-run --json
430
434
  ```
431
435
 
432
436
  `ConsentGate` changes one behavior from `Frame`. When its category is granted,
@@ -498,7 +502,7 @@ The `css-variables-to-v3` codemod renames `--consent-widget-*` and
498
502
  `theme.slots.consentDialogFooter` and marks `theme.slots.frame`.
499
503
 
500
504
  ```bash
501
- npx @c15t/cli codemods theme-to-consent-theme css-variables-to-v3 postcss-tailwind3 --dry-run --json
505
+ inth c15t codemods theme-to-consent-theme css-variables-to-v3 postcss-tailwind3 --dry-run --json
502
506
  ```
503
507
 
504
508
  `theme.slots` now applies in React. v2 accepted it in types and ignored it, so
@@ -586,7 +590,7 @@ The `dev-tools-to-c15t` codemod points the imports at `c15t/react/devtools` and
586
590
  removes the `namespace` prop:
587
591
 
588
592
  ```bash
589
- npx @c15t/cli codemods dev-tools-to-c15t --dry-run --json
593
+ inth c15t codemods dev-tools-to-c15t --dry-run --json
590
594
  ```
591
595
 
592
596
  ## Upgrade from an earlier v3 alpha
@@ -622,16 +626,19 @@ handled by the `consent-provider-options` codemod, not by aliases.
622
626
  | The build options `outputFile`, `exportName`, `importSource` and `rootDir` | Removed. The snapshot is a virtual module, or a file under `node_modules/.cache/c15t/` in Next.js. |
623
627
 
624
628
  React removes nothing besides the modes above. Import `manifest()` from
625
- `c15t/react`, next to `hosted()` and `offline()`, instead of from
629
+ `c15t/react`, next to `hosted()` and `local()`, instead of from
626
630
  `@c15t/browser/headless`; React apps need nothing from `@c15t/browser`.
627
631
 
628
632
  ## Update the CLI
629
633
 
630
- * Run `c15t login` again. The CLI signs in through Inth now and no longer
631
- reads sessions saved by v2.
632
- * `c15t setup` no longer writes `.env.local` or `.env.example` and no longer
633
- accepts `--env`. It writes the backend URL into the files it generates.
634
- * `c15t setup` no longer detects Remix or Gatsby. Pick a guide from
634
+ * Setup moved to the Inth CLI.
635
+ [Install the Inth CLI](https://c15t.com/docs/cli/overview#install-the-inth-cli), then run
636
+ `inth c15t` in place of `c15t setup` and `inth c15t codemods` in place of
637
+ `c15t codemods`.
638
+ * Sign in with `inth login`. Sessions saved by the v2 CLI aren't read.
639
+ * `inth c15t scaffold` writes the backend URL to `.env`, not `.env.local` or
640
+ `.env.example`.
641
+ * Remix and Gatsby have no CLI support. Pick a guide from
635
642
  [Choose your setup](../../concepts/choose-your-setup.md) for those apps.
636
643
 
637
644
  ## Check the migration
@@ -157,6 +157,11 @@ const xPixelAllowed = useVendorAllowed('x-pixel');
157
157
  or the vendor list changed under the edit. Call `reset()` to start again.
158
158
  `useDeclaredVendors()` returns the declared vendors, and `useVendorChoice()`
159
159
  returns the recorded vendor decision, or `null` before the visitor made one.
160
+ To list vendors under each category in your own preference center, call
161
+ `useVendorsListedUnder(category)` from `c15t/react/headless`. It returns the
162
+ vendors the stock dialog lists in that category's row, in declared order. A
163
+ vendor known only from a script's `vendor` slug stays listed after the
164
+ visitor turns it off, so they can turn it back on.
160
165
 
161
166
  A vendor reads as allowed only when it is declared, its category is allowed
162
167
  and the visitor has not switched it off. An id that nothing declares, in
@@ -138,7 +138,7 @@ sends it with `/init`, and records it on the choices of visitors the banner
138
138
  showed it to, so you can compare opt-in rate and time to decision per arm.
139
139
 
140
140
  The same option exists in `c15t/next` (`ConsentRoot` `options`), `c15t/vue`,
141
- `@c15t/svelte`, the `c15t()` Astro integration from `c15t/astro`, and
141
+ `c15t/svelte`, the `c15t()` Astro integration from `c15t/astro`, and
142
142
  `@c15t/browser`. On a server-rendered page in Next.js, TanStack Start or
143
143
  SvelteKit, pass it to `resolveConsent` instead, which counts the arm and hands
144
144
  it to the client; see [Vercel Flags SDK](#vercel-flags-sdk).
@@ -292,7 +292,7 @@ from [send the events to your own analytics](#send-the-events-to-your-own-analyt
292
292
  in `options`. The layout awaits consent inside `<Suspense>`, as in
293
293
  [render the banner in the server HTML](https://c15t.com/docs/frameworks/next/app-router#render-the-banner-in-the-server-html),
294
294
  so the banner is in the server HTML already showing the visitor's arm.
295
- `resolveConsent` in `c15t/tanstack-start/server` and `@c15t/svelte/server`
295
+ `resolveConsent` in `c15t/tanstack-start/server` and `c15t/svelte/server`
296
296
  takes the same `experiment`.
297
297
 
298
298
  If you pass `resolveConsent()` to `ConsentRoot` without awaiting it (the
@@ -520,7 +520,7 @@ server has to know the arm. Pass the experiment to `resolveConsent`, as in
520
520
  [Vercel Flags SDK](#vercel-flags-sdk): it sends only `{ id, arm }` to the
521
521
  backend and hands the full experiment to the client in its state.
522
522
  `resolveConsent` takes `experiment` in `c15t/next/server`,
523
- `c15t/tanstack-start/server` and `@c15t/svelte/server`; Astro and Nuxt pass
523
+ `c15t/tanstack-start/server` and `c15t/svelte/server`; Astro and Nuxt pass
524
524
  the arm they rendered on their own. When c15t picks the arm in the browser,
525
525
  the browser's own `/init` carries it, so a server-rendered page that skips
526
526
  the client `/init` has no count for built-in assignment. Resolve the arm with
@@ -53,7 +53,7 @@ No banner is sometimes correct. Find out which case you have:
53
53
  | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
54
54
  | The backend request failed or has a CORS error | Wrong backend URL, or your origin is not trusted | Copy the URL from your Inth project exactly, including any path. Add the site's origin, such as `https://www.example.com` or `http://localhost:3000`, to the project's trusted origins. |
55
55
  | Requests go to `your-project.inth.app` and fail | The placeholder backend URL from the guides is still in your code | Replace `https://your-project.inth.app` with the URL from your Inth project, including any path prefix. |
56
- | No backend request at all | No provider mounted, the provider runs in offline mode, or the page resolved from a bundled manifest | Check `window.c15t`. `mode: 'manifest'` with no request is expected for a policy that does not depend on location. Otherwise confirm your framework's backend URL variable is set, as [consent modes](../concepts/modes.md#set-the-backend-url) lists. |
56
+ | No backend request at all | No provider mounted, the provider runs in local mode, or the page resolved from a bundled manifest | Check `window.c15t`. `mode: 'manifest'` with no request is expected for a policy that does not depend on location. Otherwise confirm your framework's backend URL variable is set, as [consent modes](../concepts/modes.md#set-the-backend-url) lists. |
57
57
  | The policy resolved and no prompt is needed | The visitor's location maps to a policy without a prompt, such as a US state without a privacy law in the recommended rules | Expected. Test from a location that needs a prompt. Server helpers such as Next.js `resolveConsent` accept a `country` override for testing; remove it before you deploy. |
58
58
  | The policy resolved and a choice is stored | A returning visitor | Expected. Clear site data or use a private window. |
59
59
  | The banner is in the DOM but invisible or unstyled | c15t's rules are missing: `styles: false` without an imported stylesheet, a Content Security Policy that blocks c15t's `<style>` elements, Tailwind CSS 3's preflight, or a SvelteKit app without `c15tHandle` | See [stylesheets and CSS layers](../customization/stylesheets.md) for how your framework loads c15t's rules. |
@@ -135,6 +135,7 @@ messages, prefixed with the integration's name, such as
135
135
  | `could not fetch the consent manifest from <url> during the build (fetch failed: ...)`, with `ENOTFOUND`, `ECONNREFUSED` or another network error | The build cannot reach the backend | Build where the backend is reachable, or deploy with `C15T_ON_BUILD_ERROR=runtime` |
136
136
  | `... (no response within 10 seconds)` | The backend did not answer in time | Check the backend, or build where it is reachable |
137
137
  | `... (/manifest responded 404 Not Found)`, or another status | The URL is not your project's backend, such as the `https://your-project.inth.app` placeholder or a URL missing its path prefix | Copy the backend URL exactly as Inth shows it |
138
+ | `... (/manifest responded 402: <message> (<CODE>))`, or another status followed by a message | The backend refused the request and said why, such as an account out of its usage allowance | Follow the backend's message |
138
139
  | `... (/manifest returned an invalid consent manifest.)` | The URL answered with something other than a consent manifest, such as an HTML page | Point the backend URL at the backend itself, not your site or a dashboard page |
139
140
  | `no backend URL is set, so the build cannot fetch the consent manifest. Pass backendURL or set ...` | No `backendURL` option and no backend URL variable | Set the variable your framework reads; see [consent modes](../concepts/modes.md#set-the-backend-url) |
140
141
  | `skipped the consent manifest fetch because "/api/c15t" is not an absolute http(s) URL, so the server fetches the policy at runtime.` | A relative backend URL. A notice, not an error. | Pass the absolute backend URL. With `onBuildError: 'fail'`, this stops the build with `build-time manifests require an absolute upstream URL.` |
@@ -176,7 +177,7 @@ instead of a factory:
176
177
  | Message | Fix |
177
178
  | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
178
179
  | ``@c15t/react ConsentProvider: `mode` is required. Use manifest() or hosted().`` | Pass `mode`, such as `manifest()` from the same package. The start of the message names the package and the API that got no mode, such as `@c15t/svelte ConsentProvider` or `@c15t/core createConsentRuntime()`. Production builds name the package only: ``@c15t/react: `mode` is required.`` |
179
- | ``@c15t/browser: `mode` must be a factory, such as manifest(), hosted() or offline() from @c15t/browser. Mode names like "hosted" work only in the script-tag builds.`` | Import the factory and call it: `init({ mode: hosted() })`. |
180
+ | ``@c15t/browser: `mode` must be a factory, such as manifest(), hosted() or local() from @c15t/browser. Mode names like "hosted" work only in the script-tag builds.`` | Import the factory and call it: `init({ mode: hosted() })`. |
180
181
  | ``c15t: hosted() needs `backendURL`. Pass it, or add consentManifest() from c15t/build to your Vite config and set VITE_C15T_BACKEND_URL (or VITE_INTH_PROJECT_URL).`` | The React `hosted()` found no backend URL. `@c15t/browser` throws the same message prefixed `@c15t/browser:`. A production build prints only the first sentence. |
181
182
  | ``c15t: `mode` is the hosted() transport itself, so its code ships in the client bundle. Pass the hosted() your framework package exports instead: it is plain data, and the root loads the code only when it runs.`` | A warning outside production. A server-rendered root got a transport from `c15t/react`. Import the mode from your framework package. Next.js has its own wording; see [Next.js troubleshooting](https://c15t.com/docs/frameworks/next/troubleshooting). |
182
183
 
@@ -172,7 +172,7 @@ pass them as a top-level prop:
172
172
 
173
173
  ```svelte title="src/App.svelte"
174
174
  <script lang="ts">
175
- import { ConsentProvider, manifest } from '@c15t/svelte';
175
+ import { ConsentProvider, manifest } from 'c15t/svelte';
176
176
  import { scripts } from './consent-scripts';
177
177
  </script>
178
178
 
@@ -191,7 +191,7 @@ handle and the layout load from the [SvelteKit quickstart](https://c15t.com/docs
191
191
 
192
192
  ```svelte title="src/routes/+layout.svelte"
193
193
  <script lang="ts">
194
- import { ConsentRoot } from '@c15t/svelte';
194
+ import { ConsentRoot } from 'c15t/svelte';
195
195
  import { scripts } from '../consent-scripts';
196
196
 
197
197
  let { children, data } = $props();
@@ -164,7 +164,7 @@ pass them as a top-level prop:
164
164
 
165
165
  ```svelte title="src/App.svelte"
166
166
  <script lang="ts">
167
- import { ConsentProvider, manifest } from '@c15t/svelte';
167
+ import { ConsentProvider, manifest } from 'c15t/svelte';
168
168
  import { scripts } from './consent-scripts';
169
169
  </script>
170
170
 
@@ -183,7 +183,7 @@ handle and the layout load from the [SvelteKit quickstart](https://c15t.com/docs
183
183
 
184
184
  ```svelte title="src/routes/+layout.svelte"
185
185
  <script lang="ts">
186
- import { ConsentRoot } from '@c15t/svelte';
186
+ import { ConsentRoot } from 'c15t/svelte';
187
187
  import { scripts } from '../consent-scripts';
188
188
 
189
189
  let { children, data } = $props();