@c15t/react 3.0.0-alpha.3 → 3.0.0-alpha.4

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 (183) hide show
  1. package/AGENTS.md +91 -72
  2. package/README.md +21 -17
  3. package/SKILL.md +34 -0
  4. package/dist/component-hooks/use-headless-consent-ui.js +1 -1
  5. package/dist/components/consent-gate/atoms.js +1 -1
  6. package/dist/components/consent-gate/consent-gate.js +1 -1
  7. package/dist/components/iab-panel/atoms/card.js +1 -1
  8. package/dist/components/iab-panel/atoms/feature-item.js +2 -0
  9. package/dist/components/iab-panel/atoms/header.js +1 -1
  10. package/dist/components/iab-panel/iab-panel.js +1 -1
  11. package/dist/components/iab-panel/index.js +1 -1
  12. package/dist/components/iab-prompt/atoms/overlay.js +1 -1
  13. package/dist/components/iab-prompt/atoms/root.js +1 -1
  14. package/dist/components/panel/atoms/card.js +1 -1
  15. package/dist/components/panel/atoms/root.js +1 -1
  16. package/dist/components/panel-trigger/atoms/button.js +1 -1
  17. package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
  18. package/dist/components/preferences/atoms/accordion.js +1 -1
  19. package/dist/components/prompt/atoms/overlay.js +1 -1
  20. package/dist/components/prompt/atoms/root.js +1 -1
  21. package/dist/components/prompt/components.js +1 -1
  22. package/dist/components/shared/libs/compose-refs.js +1 -0
  23. package/dist/components/shared/libs/slot.js +1 -1
  24. package/dist/components/shared/primitives/button.js +1 -1
  25. package/dist/components/shared/ui/dialog/dialog.js +1 -1
  26. package/dist/components/shared/ui/icon.js +1 -1
  27. package/dist/context/consent-tracking-context.js +1 -1
  28. package/dist/devtools.js +1 -1
  29. package/dist/draft.js +1 -1
  30. package/dist/hooks/use-focus-trap.js +1 -1
  31. package/dist/hooks.js +1 -1
  32. package/dist/iab/styles.tw3.css +4 -4
  33. package/dist/index.js +1 -1
  34. package/dist/module-hooks/iframe-blocker.js +1 -1
  35. package/dist/postcss-tailwind3.js +1 -0
  36. package/dist/provider.js +1 -1
  37. package/dist/server/fetch-ssr-data.js +1 -1
  38. package/dist/server/headers.js +1 -1
  39. package/dist/server/normalize-url.js +1 -1
  40. package/dist/styles.tw3.css +16 -16
  41. package/dist/transports/offline.js +1 -1
  42. package/dist/ui-save.js +1 -1
  43. package/dist/utils/merge-slot-props.js +1 -1
  44. package/dist/utils/theme-utils.js +1 -1
  45. package/dist/version.js +1 -1
  46. package/dist-types/component-hooks/use-headless-consent-ui.d.ts +1 -1
  47. package/dist-types/components/iab-panel/atoms/feature-item.d.ts +28 -0
  48. package/dist-types/components/iab-panel/iab-panel.d.ts +2 -2
  49. package/dist-types/components/iab-panel/index.d.ts +5 -2
  50. package/dist-types/components/iab-panel/use-iab-translations.d.ts +4 -0
  51. package/dist-types/components/iab-prompt/iab-prompt.d.ts +2 -2
  52. package/dist-types/components/iab-prompt/index.d.ts +2 -2
  53. package/dist-types/components/panel/atoms/card.d.ts +1 -0
  54. package/dist-types/components/prompt/components.d.ts +6 -0
  55. package/dist-types/components/shared/libs/compose-refs.d.ts +20 -0
  56. package/dist-types/components/shared/ui/icon.d.ts +2 -1
  57. package/dist-types/context/consent-tracking-context.d.ts +10 -0
  58. package/dist-types/context/theme-context.d.ts +2 -2
  59. package/dist-types/context.d.ts +6 -0
  60. package/dist-types/devtools.d.ts +13 -2
  61. package/dist-types/draft.d.ts +10 -4
  62. package/dist-types/hooks/use-focus-trap.d.ts +3 -1
  63. package/dist-types/hooks.d.ts +54 -13
  64. package/dist-types/iab.d.ts +1 -1
  65. package/dist-types/index.d.ts +2 -2
  66. package/dist-types/postcss-tailwind3.d.ts +21 -0
  67. package/dist-types/provider.d.ts +22 -5
  68. package/dist-types/server/headers.d.ts +14 -4
  69. package/dist-types/server/normalize-url.d.ts +14 -7
  70. package/dist-types/server/types.d.ts +12 -1
  71. package/dist-types/transports/offline.d.ts +7 -16
  72. package/dist-types/ui-config-context.d.ts +7 -1
  73. package/dist-types/utils/merge-slot-props.d.ts +15 -1
  74. package/dist-types/utils/theme-utils.d.ts +17 -2
  75. package/dist-types/version.d.ts +1 -1
  76. package/docs/README.md +91 -72
  77. package/docs/assets/v3/bottom-bar.png +0 -0
  78. package/docs/assets/v3/brand-card.png +0 -0
  79. package/docs/assets/v3/brand-preferences.png +0 -0
  80. package/docs/assets/v3/choice-wall.png +0 -0
  81. package/docs/assets/v3/headless-bar-html.png +0 -0
  82. package/docs/assets/v3/headless-bar-mobile.png +0 -0
  83. package/docs/assets/v3/headless-bar.png +0 -0
  84. package/docs/assets/v3/slim-bar.png +0 -0
  85. package/docs/concepts/choose-your-setup.md +87 -0
  86. package/docs/{frameworks/react/concepts → concepts}/consent-categories.md +22 -7
  87. package/docs/{guides → concepts}/consent-state.md +71 -101
  88. package/docs/{guides → concepts}/data-fetching.md +31 -27
  89. package/docs/concepts/how-consent-works.md +123 -0
  90. package/docs/{frameworks/react/concepts/policy-presets.md → concepts/policies.md} +7 -59
  91. package/docs/customization/class-names.md +202 -0
  92. package/docs/customization/dark-mode.md +157 -0
  93. package/docs/customization/motion.md +119 -0
  94. package/docs/customization/overview.md +67 -34
  95. package/docs/customization/recipes.md +839 -49
  96. package/docs/customization/slots.md +216 -35
  97. package/docs/customization/stylesheets.md +147 -0
  98. package/docs/customization/tailwind.md +842 -0
  99. package/docs/customization/tokens.md +163 -96
  100. package/docs/customization/translations.md +61 -3
  101. package/docs/frameworks/react/callbacks.md +118 -0
  102. package/docs/frameworks/react/clear-on-revocation.md +106 -0
  103. package/docs/frameworks/react/components/consent-banner.md +42 -28
  104. package/docs/frameworks/react/components/consent-dialog-link.md +13 -11
  105. package/docs/frameworks/react/components/consent-dialog-trigger.md +81 -76
  106. package/docs/frameworks/react/components/consent-dialog.md +56 -50
  107. package/docs/frameworks/react/components/consent-gate.md +37 -20
  108. package/docs/frameworks/react/components/consent-provider.md +126 -0
  109. package/docs/frameworks/react/components/consent-widget.md +23 -20
  110. package/docs/frameworks/react/components/dev-tools.md +52 -84
  111. package/docs/frameworks/react/components.md +105 -0
  112. package/docs/frameworks/react/compose.md +354 -0
  113. package/docs/frameworks/react/content-security-policy.md +126 -0
  114. package/docs/frameworks/react/customize.md +250 -0
  115. package/docs/frameworks/react/embeds.md +89 -0
  116. package/docs/frameworks/react/{hooks/overview.md → hooks.md} +21 -16
  117. package/docs/frameworks/react/{iab/overview.md → iab.md} +80 -31
  118. package/docs/frameworks/react/network-blocker.md +35 -41
  119. package/docs/frameworks/react/quickstart.md +139 -101
  120. package/docs/frameworks/react/rendering.md +128 -0
  121. package/docs/frameworks/react/scripts.md +115 -0
  122. package/docs/frameworks/react/translations.md +155 -0
  123. package/docs/frameworks/react/troubleshooting.md +92 -47
  124. package/docs/frameworks/react/vendor-consent.md +170 -0
  125. package/docs/guides/banner-experiments.md +654 -0
  126. package/docs/guides/troubleshooting.md +120 -47
  127. package/docs/guides/verify-consent.md +81 -49
  128. package/docs/integrations/adobe-analytics.md +167 -159
  129. package/docs/integrations/ahrefs-analytics.md +152 -154
  130. package/docs/integrations/amplitude.md +162 -156
  131. package/docs/integrations/building-integrations.md +136 -37
  132. package/docs/integrations/clearbit.md +154 -154
  133. package/docs/integrations/cloudflare-web-analytics.md +156 -156
  134. package/docs/integrations/cloudflare-zaraz.md +209 -261
  135. package/docs/integrations/crisp.md +164 -158
  136. package/docs/integrations/databuddy.md +157 -173
  137. package/docs/integrations/fathom-analytics.md +158 -156
  138. package/docs/integrations/front-chat.md +167 -167
  139. package/docs/integrations/google-maps.md +118 -83
  140. package/docs/integrations/google-tag-manager.md +178 -163
  141. package/docs/integrations/google-tag.md +163 -160
  142. package/docs/integrations/heap.md +163 -155
  143. package/docs/integrations/hightouch.md +161 -157
  144. package/docs/integrations/hotjar.md +159 -155
  145. package/docs/integrations/intercom.md +183 -153
  146. package/docs/integrations/klaviyo.md +486 -0
  147. package/docs/integrations/linkedin-insights.md +174 -150
  148. package/docs/integrations/logrocket.md +160 -156
  149. package/docs/integrations/matomo-analytics.md +188 -178
  150. package/docs/integrations/meta-pixel.md +188 -150
  151. package/docs/integrations/microsoft-clarity.md +163 -155
  152. package/docs/integrations/microsoft-uet.md +148 -154
  153. package/docs/integrations/mixpanel-analytics.md +155 -160
  154. package/docs/integrations/one-dollar-stats.md +166 -165
  155. package/docs/integrations/openai-pixel.md +204 -301
  156. package/docs/integrations/overview.md +137 -80
  157. package/docs/integrations/pinterest-tag.md +191 -183
  158. package/docs/integrations/pirsch.md +169 -159
  159. package/docs/integrations/plausible-analytics.md +172 -158
  160. package/docs/integrations/posthog.md +226 -241
  161. package/docs/integrations/promptwatch.md +154 -154
  162. package/docs/integrations/reddit-pixel.md +185 -157
  163. package/docs/integrations/rudderstack.md +201 -186
  164. package/docs/integrations/rybbit-analytics.md +171 -160
  165. package/docs/integrations/segment.md +182 -154
  166. package/docs/integrations/snapchat-pixel.md +186 -156
  167. package/docs/integrations/tiktok-pixel.md +171 -150
  168. package/docs/integrations/umami-analytics.md +163 -158
  169. package/docs/integrations/vercel-analytics.md +166 -157
  170. package/docs/integrations/x-pixel.md +176 -150
  171. package/docs/integrations/youtube.md +121 -86
  172. package/docs/upgrade-v3.md +475 -467
  173. package/package.json +15 -9
  174. package/docs/assets/v3/brand-bar.png +0 -0
  175. package/docs/assets/v3/mobile-card.png +0 -0
  176. package/docs/assets/v3/preferences.png +0 -0
  177. package/docs/frameworks/react/components/consent-manager-provider.md +0 -95
  178. package/docs/frameworks/react/script-loader.md +0 -69
  179. package/docs/frameworks/react/styling/overview.md +0 -266
  180. package/docs/guides/deployment-modes.md +0 -75
  181. package/docs/guides/shared-consent-controls.md +0 -158
  182. package/docs/integrations/clear-on-revocation.md +0 -167
  183. package/docs/integrations/granular-consent.md +0 -210
@@ -0,0 +1,155 @@
1
+ ---
2
+ title: Translations
3
+ description: Where c15t banner and dialog copy comes from in a React app, how
4
+ the browser picks the visitor's language, and how to override copy and switch
5
+ languages with the provider's i18n option and hooks.
6
+ group: frameworks
7
+ ---
8
+
9
+ ## Where the copy comes from
10
+
11
+ The banner, dialog, widget and gate placeholder read their text from one set
12
+ of translations for the visitor's language. Which set depends on the
13
+ provider's `mode`:
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 |
19
+
20
+ With a backend, edit wording in your Inth project first, so it changes without
21
+ a deploy. Code can override that copy in two places:
22
+
23
+ * `i18n` in the `ConsentProvider` options changes a message everywhere it
24
+ appears.
25
+ * Text props on `ConsentBanner` change one banner.
26
+
27
+ [Copy and translations](../../customization/translations.md) lists the message
28
+ keys and explains how code messages combine with the project's copy.
29
+
30
+ ## How c15t picks the language
31
+
32
+ In `hosted()` mode the browser's `/init` request carries its own
33
+ `Accept-Language` header, and the backend answers with copy in that language.
34
+ When your project has no copy for it, the backend returns its fallback
35
+ language. Set `overrides.language` in the provider options to request a
36
+ specific language instead.
37
+
38
+ In `offline()` mode, `i18n.locale` picks the language, `en` by default. The
39
+ browser's language is not read.
40
+
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.
44
+
45
+ ## Change the copy in code
46
+
47
+ Keep the messages in their own module:
48
+
49
+ ```ts title="src/consent-i18n.ts"
50
+ import type { ConsentProviderOptions } from 'c15t/react';
51
+
52
+ // Keys you leave out keep the copy from your Inth project.
53
+ export const i18n = {
54
+ messages: {
55
+ de: {
56
+ common: { rejectAll: 'Optionale ablehnen' },
57
+ cookieBanner: { title: 'Cookies auf dieser Website' },
58
+ },
59
+ en: {
60
+ common: { rejectAll: 'Reject optional' },
61
+ cookieBanner: { title: 'Cookies on this site' },
62
+ },
63
+ },
64
+ } satisfies ConsentProviderOptions['i18n'];
65
+ ```
66
+
67
+ Pass them to the provider in the file that renders it, such as
68
+ `src/consent.tsx` from the [quickstart](./quickstart.md):
69
+
70
+ ```tsx title="src/consent.tsx"
71
+ <ConsentProvider options={{ mode, scripts, i18n }}>
72
+ ```
73
+
74
+ For the visitor's language, your messages replace the backend's copy key by
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
77
+ keys you leave out for another language show in English.
78
+
79
+ The provider reads `i18n` once, when it mounts. In development it logs a
80
+ warning if `i18n` changes later. Define the object at module level, as above,
81
+ rather than inside a component.
82
+
83
+ ## Change one banner's copy
84
+
85
+ `ConsentBanner` takes `title`, `description`, `acceptButtonText`,
86
+ `rejectButtonText`, `customizeButtonText` and `dismissButtonText`. The props
87
+ replace the text in every language, so pass values in the page's language:
88
+
89
+ ```tsx
90
+ <ConsentBanner
91
+ title="Cookies on this site"
92
+ rejectButtonText="Reject optional"
93
+ />;
94
+ ```
95
+
96
+ ## Switch the language at runtime
97
+
98
+ `useSetLanguage()` stores the new language, and `useInit()` resolves the policy
99
+ again, which fetches the copy for that language. Call both from your language
100
+ switcher:
101
+
102
+ ```tsx title="src/language-switcher.tsx"
103
+ import { useInit, useSetLanguage, useSnapshot } from 'c15t/react';
104
+
105
+ const languages = [
106
+ { code: 'en', label: 'English' },
107
+ { code: 'de', label: 'Deutsch' },
108
+ ];
109
+
110
+ export const LanguageSwitcher = () => {
111
+ const setLanguage = useSetLanguage();
112
+ const init = useInit();
113
+ const language = useSnapshot().translations?.language ?? 'en';
114
+
115
+ return (
116
+ <select
117
+ aria-label="Consent language"
118
+ value={language}
119
+ onChange={(event) => {
120
+ setLanguage(event.target.value);
121
+ // Setting the language alone keeps the current copy. Init fetches
122
+ // the policy and copy for the new language.
123
+ void init();
124
+ }}
125
+ >
126
+ {languages.map(({ code, label }) => (
127
+ <option key={code} value={code}>
128
+ {label}
129
+ </option>
130
+ ))}
131
+ </select>
132
+ );
133
+ };
134
+ ```
135
+
136
+ Setting the language without calling `init()` changes nothing on screen. Your
137
+ `i18n` messages for the new language apply on top of the copy that arrives.
138
+ Render the switcher anywhere inside `ConsentProvider`.
139
+
140
+ Switching needs a backend. In `offline()` mode the copy stays in the
141
+ `i18n.locale` language.
142
+
143
+ If your site changes language through its own i18n library, you can instead
144
+ pass its locale as `overrides.language` in the provider options. When that
145
+ value changes, the provider resolves the policy again in the new language.
146
+
147
+ ## Verify
148
+
149
+ 1. Set your browser language to one your project translates, clear site data
150
+ and reload under a policy that shows a banner. DevTools Network shows the
151
+ `/init` request with that `Accept-Language` header, and the banner uses
152
+ that language.
153
+ 2. Check that a message you set in `i18n` shows in the banner.
154
+ 3. Switch the language with your switcher. A new `/init` request runs, and the
155
+ banner and dialog text change without a reload.
@@ -1,67 +1,112 @@
1
1
  ---
2
- title: Troubleshoot React consent
3
- description: Diagnose policy resolution, early scripts, storage and rendering problems.
2
+ title: Troubleshooting
3
+ description: Fix a missing banner, early vendor requests, unstyled components
4
+ and theme warnings in a React app that uses c15t/react.
4
5
  group: frameworks
5
6
  ---
6
7
 
7
- ## Why is there no banner?
8
+ ## The /init request goes to your-project.inth.app
8
9
 
9
- Inspect `resolution` and `promptRequirement` before changing styles. Pending or
10
- failed initialization, no matching rule, a valid stored choice and a rule that
11
- requires no prompt can all produce no banner for different reasons. A missing
12
- stylesheet can also make rendered controls appear incorrectly.
10
+ **Check.** In DevTools Network, the `GET` request to `/init` goes to
11
+ `your-project.inth.app` and fails.
13
12
 
14
- Check the backend URL and Network response, then confirm the active policy for
15
- the visitor's location. Do not solve missing UI by granting every category or
16
- setting `enabled: false`: disabling the runtime permits optional loading.
13
+ **Fix.** Replace the placeholder URL in `hosted({ url })` in `src/consent.tsx`
14
+ with the backend URL from your Inth project, exactly as Inth shows it, then
15
+ rebuild.
17
16
 
18
- ## Why does the UI disappear with a content blocker?
17
+ ## There is no banner
19
18
 
20
- Check the browser Network panel for `ERR_BLOCKED_BY_CLIENT` or a failed dynamic
21
- import. Older c15t builds used component filenames such as
22
- `consent-dialog-*.js`, which some cookie-annoyance lists block. Update the c15t
23
- packages and rebuild the app. Current component modules use neutral filenames;
24
- public component imports stay the same. This also covers Vite development
25
- requests used by TanStack Start and other Vite integrations.
19
+ **Check.** In DevTools Network, find the `GET` request to `/init` on your
20
+ backend URL.
26
21
 
27
- Extensions can separately hide elements with cosmetic filters or block a
28
- configured backend URL. Check the failed request or hidden element to distinguish
29
- those cases from a missing component module.
22
+ | What you see | Cause | Fix |
23
+ | ------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
24
+ | No `/init` request | `ConsentProvider` is not mounted, or an error stopped `src/consent.tsx` from rendering | Check the console for errors and render one provider at the app root, as in the [quickstart](./quickstart.md) |
25
+ | `/init` fails with a CORS error | Your site's origin is not trusted | Add the exact origin, including port in development, to the trusted origins in your Inth project |
26
+ | `/init` returns 200 | The policy for this visitor asks for no banner, or a stored choice is still valid | Inspect the active policy with the component below, or clear site data |
30
27
 
31
- ## Why does analytics run before a choice?
28
+ A policy with `prompt: 'none'` shows no banner by design, and so does a
29
+ returning visitor with a valid stored choice. [Policies](../../concepts/policies.md)
30
+ explains why a region may not show one.
32
31
 
33
- Check effective permission under the selected policy, then find every loader
34
- for that vendor. Remove unconditional script tags, framework analytics plugins
35
- and duplicate tag-manager entries. c15t's script registration only controls the
36
- scripts registered with it.
32
+ ## Inspect the active policy
37
33
 
38
- Google helpers intentionally load with Consent Mode defaults. A Google request
39
- is not by itself proof that its storage consent was granted. If your requirement
40
- is no request at all, do not use an always-loaded helper unchanged.
34
+ Render this component inside `ConsentProvider`. It reads state and records no
35
+ choice:
41
36
 
42
- ## Why does a choice disappear on reload?
37
+ ```tsx title="src/consent-policy-debug.tsx"
38
+ import { useSnapshot } from 'c15t/react';
43
39
 
44
- Check whether persistence is disabled, browser storage is blocked, the origin
45
- changed, or the receipt expired or no longer matches the current policy.
46
- A development example with `persistence: false` deliberately resets on reload.
47
- Do not "repair" persistence by saving permissions automatically on mount.
40
+ export function ConsentPolicyDebug() {
41
+ const snapshot = useSnapshot();
42
+ const resolution = snapshot.resolution;
48
43
 
49
- ## Why does hydration differ from server HTML?
44
+ return (
45
+ <pre>
46
+ {JSON.stringify(
47
+ {
48
+ status: resolution.status,
49
+ policy: resolution.status === 'matched' ? resolution.policy.id : null,
50
+ prompt: snapshot.promptRequirement,
51
+ permissions: snapshot.effectivePermissions,
52
+ location: snapshot.location,
53
+ privacySignals: snapshot.privacySignals,
54
+ },
55
+ null,
56
+ 2
57
+ )}
58
+ </pre>
59
+ );
60
+ }
61
+ ```
50
62
 
51
- Use the adapter's request helper and pass the returned configuration unchanged
52
- to its boundary. Check that server and browser use the same backend and policy
53
- inputs. A module-level mutable runtime on a server can share one visitor's state
54
- with another request; create request-owned state instead.
63
+ The component re-renders on every consent change. Remove it after you finish.
64
+ For a feature gate, use `useConsent('marketing')` or the category you need.
55
65
 
56
- ## Why does static hosting fail when development works?
66
+ ## A vendor loads before the visitor chooses
57
67
 
58
- A static host has no app server for init routes, proxies or server functions.
59
- Use absolute external consent URLs or an explicitly local policy. Test the
60
- production output with a static file server, not the framework dev server.
68
+ **Check.** Search `index.html`, your entry module and any tag manager for the
69
+ vendor's script or SDK initializer.
61
70
 
62
- ## Why does customization do nothing?
71
+ **Fix.** Remove every loader except the one in `options.scripts`. c15t only
72
+ gates scripts registered with it, and the network blocker cannot stop code that
73
+ runs before the provider renders. [Scripts and embeds](./scripts.md)
74
+ covers the options.
63
75
 
64
- Check the imported stylesheet, the correct token or slot, and which element
65
- carries the state attribute. `data-variant` on a banner root is not a matching
66
- attribute on its child card. Check cascade layers and the Tailwind version
67
- before adding specificity. See [customization](../../customization/overview.md).
76
+ ## The banner has no styles
77
+
78
+ **Check.** In DevTools Elements, the banner has classes but no rules apply.
79
+
80
+ **Fix.** Import `c15t/react/styles.css` once in the module that renders the
81
+ provider. With Tailwind 3, keep that import above the `@tailwind` directives
82
+ and add `c15t/postcss-tailwind3` before `tailwindcss` in your PostCSS
83
+ config. Without the plugin, Tailwind 3 drops c15t's rules or fails the build
84
+ with `` `@layer components` is used but no matching `@tailwind components` directive is present ``.
85
+ See [Tailwind CSS](../../customization/tailwind.md#set-up-tailwind-css-3).
86
+
87
+ ## The console warns that theme tokens are no longer turned into CSS
88
+
89
+ **Check.** `options.theme` holds colors, radius or other tokens, and the page
90
+ has no `c15t-theme` style element. The provider only reads `consentActions`
91
+ and slot styles from `theme`.
92
+
93
+ **Fix.** Move the tokens to your CSS or render `ConsentTheme`, as shown in
94
+ [customize](./customize.md#change-colors-fonts-and-radius).
95
+
96
+ ## The banner is missing from server-rendered HTML
97
+
98
+ **Check.** Your app uses React Router framework mode, Remix or another
99
+ server-rendered setup without a c15t adapter. There this is expected: the
100
+ browser resolves consent after hydration.
101
+
102
+ **Fix.** None is needed for consent to work.
103
+ [Rendering](./rendering.md#react-router-remix-and-other-server-rendered-apps)
104
+ explains the options.
105
+
106
+ ## More help
107
+
108
+ [Troubleshoot consent](../../guides/troubleshooting.md) covers problems shared by
109
+ every framework, such as a missing banner, analytics that load before a choice,
110
+ imports that fail because npm installed c15t v2, choices that disappear on
111
+ reload, server HTML that differs from the browser, static builds that fail and
112
+ content blockers that hide the consent UI.
@@ -0,0 +1,170 @@
1
+ ---
2
+ title: Vendor consent
3
+ description: Let visitors allow a category such as marketing in a React app and
4
+ still turn off one vendor in it, with the vendors option on ConsentProvider
5
+ and useVendorAllowed.
6
+ group: frameworks
7
+ ---
8
+
9
+ ## How vendor consent works
10
+
11
+ A visitor allows marketing, then switches off X Pixel. Other marketing
12
+ scripts load; X Pixel does not. c15t loads a script, network request or
13
+ iframe that names a vendor only when both are true:
14
+
15
+ * Its category is allowed.
16
+ * The visitor has not switched its vendor off.
17
+
18
+ A vendor switch never grants a category. With marketing denied, X Pixel stays
19
+ blocked whatever its own switch says. You do not need IAB TCF for this. Under
20
+ an `iab` policy, c15t ignores vendor slugs and takes vendor consent from the
21
+ TC string instead; see [IAB TCF](./iab.md).
22
+
23
+ ## Declare the vendors
24
+
25
+ Declare the vendors next to the scripts in `src/scripts.ts`, using the `vendor`
26
+ slug each script already carries:
27
+
28
+ ```ts title="src/scripts.ts (partial)"
29
+ import type { Vendor } from 'c15t';
30
+
31
+ export const vendors: Vendor[] = [
32
+ {
33
+ id: 'x-pixel',
34
+ name: 'X Pixel',
35
+ category: 'marketing',
36
+ privacyPolicyUrl: 'https://x.com/privacy',
37
+ },
38
+ ];
39
+ ```
40
+
41
+ Then add them to the provider options in `src/consent.tsx`:
42
+
43
+ ```tsx title="src/consent.tsx (partial)"
44
+ import { scripts, vendors } from './scripts';
45
+
46
+ <ConsentProvider options={{ mode, scripts, vendors }}>
47
+ ```
48
+
49
+ `vendors` is a live option, like `scripts`: passing a new list replaces the
50
+ declared vendors. A provider that borrows a `runtime` does not accept
51
+ `vendors`; declare them in `createConsentRuntime({ vendors })` instead.
52
+
53
+ ## Vendor fields
54
+
55
+ | Field | Required | Behavior |
56
+ | ----------------------------------------- | -------- | --------------------------------------------------------------------------- |
57
+ | `id` | Yes | Lowercase slug of up to 64 characters: letters, digits, `.`, `_` and `-`. |
58
+ | `name` | Yes | Name shown in the preference dialog. |
59
+ | `category` | Yes | A category, or a condition such as `{ or: ['measurement', 'marketing'] }`. |
60
+ | `privacyPolicyUrl` | Yes | Link shown next to the vendor. |
61
+ | `description`, `legalName`, `homepageUrl` | No | Extra detail shown on the vendor card. |
62
+ | `disabled` | No | List the vendor without a switch. A stored denial for it no longer applies. |
63
+
64
+ The `id` must match the `vendor` slug on the script. Every `@c15t/integrations`
65
+ helper sets `vendor` to its script ID, so `xPixel()` is `x-pixel`, `gtag()`
66
+ is `gtag` and `cloudflareZaraz()` is `cloudflare-zaraz`. Each
67
+ [vendor guide](../../integrations/overview.md) names its slug.
68
+
69
+ A self-hosted backend can declare vendors too. `/init` returns them and c15t
70
+ merges them with the vendors in code. When both declare the same `id`, the
71
+ code declaration wins.
72
+
73
+ ## Gate scripts, requests and iframes by vendor
74
+
75
+ Put the vendor slug on each target:
76
+
77
+ | Target | Field |
78
+ | -------------------- | ----------------------------------------- |
79
+ | Script configuration | `vendor: 'x-pixel'` |
80
+ | Network blocker rule | `vendor: 'x-pixel'` |
81
+ | Iframe blocker | `data-vendor="x-pixel"` on the `<iframe>` |
82
+
83
+ An iframe with `data-vendor` and no `data-category` is gated on the vendor
84
+ alone. A script with `alwaysLoad` still loads when its vendor is off. Its
85
+ callbacks receive `info.vendor.granted`, so the SDK can stop itself. The
86
+ Google Consent Mode, RudderStack and Cloudflare Zaraz helpers do this for
87
+ you. While their vendor is off, they send every optional category as denied.
88
+
89
+ `ConsentGate` checks a category only. To render a component only while one
90
+ vendor is allowed, read `useVendorAllowed('youtube')` and render it when the
91
+ hook returns `true`. [Embeds](./embeds.md) covers the iframe
92
+ blocker's `data-vendor` attribute.
93
+
94
+ ## What the preference dialog shows
95
+
96
+ `ConsentDialog` and `ConsentWidget` list each declared vendor under its
97
+ category, with its own switch.
98
+
99
+ The dialog lists only vendors that have a `name` and a `privacyPolicyUrl`.
100
+ A vendor's switch is disabled while its category is off. Save records the
101
+ vendors the visitor changed. Accept All and Reject All clear every vendor
102
+ denial, so each vendor follows its category again. The labels come from
103
+ `consentManagerDialog.vendors` in the translations: `title`,
104
+ `privacyPolicy`, `disabledByCategory` and `switchLabel`.
105
+
106
+ ## Build your own vendor switch
107
+
108
+ `useVendorDraft()` from `c15t/react` reads and stages vendor switches on the
109
+ same draft the category switches use, so one save records both.
110
+ `useVendorAllowed(id)` returns whether a vendor may run now: it is declared,
111
+ its category is allowed and the visitor has not switched it off.
112
+
113
+ ```tsx title="src/x-pixel-switch.tsx (partial)"
114
+ import { useVendorAllowed, useVendorDraft } from 'c15t/react';
115
+
116
+ const { vendors, setVendor, save } = useVendorDraft();
117
+ const xPixelAllowed = useVendorAllowed('x-pixel');
118
+
119
+ <input
120
+ type="checkbox"
121
+ checked={vendors['x-pixel'] ?? true}
122
+ onChange={(event) => setVendor('x-pixel', event.target.checked)}
123
+ />
124
+ <button type="button" onClick={() => save()}>Save</button>
125
+ ```
126
+
127
+ `save()` resolves `{ ok: false }` without recording anything when the policy
128
+ or the vendor list changed under the edit. Call `reset()` to start again.
129
+ `useDeclaredVendors()` returns the declared vendors, and `useVendorChoice()`
130
+ returns the recorded vendor decision, or `null` before the visitor made one.
131
+
132
+ A vendor reads as allowed only when it is declared, its category is allowed
133
+ and the visitor has not switched it off. An id that nothing declares, in
134
+ `vendors`, on a script or iframe, or from the backend, reads as not allowed.
135
+ A typo such as `x-pixle` or a vendor you forgot to declare never looks like
136
+ consent. In development, c15t logs a console warning that names the id.
137
+
138
+ ## What c15t stores
139
+
140
+ c15t stores only the vendors a visitor switched off, and sends the full
141
+ vendor map to the backend with the consent record. Under an opt-out policy,
142
+ every vendor starts on. Turning a vendor off that was on reloads the page,
143
+ the same as withdrawing a category, so code the vendor already ran stops.
144
+
145
+ ## What switching a vendor off does not do
146
+
147
+ * It does not delete cookies the vendor already set.
148
+ [Clear on revocation](./clear-on-revocation.md) does not run, because the
149
+ category stays allowed. Delete the vendor's cookies from the script's
150
+ `onConsentChange` when `info.vendor.granted` is `false`.
151
+ * It does not expire. The switch stays off until the visitor changes it or
152
+ uses Accept All or Reject All, even across a policy change.
153
+ * Adding a vendor does not ask returning visitors again. A new vendor starts
154
+ on inside an allowed category. Change the policy's `copyRevision` if a new
155
+ vendor should prompt again.
156
+
157
+ ## Verify vendor consent
158
+
159
+ Test the production build in a private window with DevTools Network open and
160
+ filtered to `ads-twitter.com`:
161
+
162
+ 1. Declare `x-pixel` and register `xPixel()`. Open Privacy settings. The
163
+ marketing row lists X Pixel with a switch.
164
+ 2. Allow marketing and save. `uwt.js` loads.
165
+ 3. Open Privacy settings, switch X Pixel off and save. c15t reloads the
166
+ page. After the reload there is no `uwt.js` request, and other marketing
167
+ scripts still load.
168
+ 4. Reload again. The X Pixel switch is still off.
169
+ 5. Turn marketing off. The X Pixel switch is disabled.
170
+ 6. Open Privacy settings and click Accept All. `uwt.js` loads again.