@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
@@ -151,8 +151,8 @@ export interface ConsentBannerProps {
151
151
  /**
152
152
  * Placement of the prompt. Overrides `presentation.prompt.position`.
153
153
  * @remarks Must be valid for the resolved variant; otherwise the variant
154
- * default is used and a development diagnostic is logged. Host-chosen
155
- * positions are never mirrored for right-to-left text.
154
+ * default is used and a development diagnostic is logged. Right-to-left
155
+ * text does not move the prompt.
156
156
  */
157
157
  position?: PromptPosition;
158
158
  /**
@@ -164,7 +164,8 @@ export interface ConsentBannerProps {
164
164
  }
165
165
  /**
166
166
  * The stock banner. It reads the policy to choose its buttons, so all of
167
- * it renders where the server can stream it: see `StreamedSurface`.
167
+ * it renders where the server can stream it: see
168
+ * `StreamedConsentSurface`.
168
169
  */
169
170
  export declare const ConsentBanner: FC<ConsentBannerProps>;
170
171
  /**
@@ -9,7 +9,7 @@ import type { PromptPosition, PromptVariant } from '@c15t/core';
9
9
  export interface ConsentBannerSurface {
10
10
  /** Shape of the first-layer prompt. */
11
11
  variant: PromptVariant;
12
- /** Placement, already mirrored for right-to-left text when defaulted. */
12
+ /** Placement. It stays the same for right-to-left text. */
13
13
  position: PromptPosition;
14
14
  /** Whether the host chose the position or the resolver defaulted it. */
15
15
  positionSource: 'host' | 'default';
@@ -30,16 +30,3 @@ export declare const ConsentBannerSurfaceContext: import("react").Context<Consen
30
30
  * @public
31
31
  */
32
32
  export declare const useConsentBannerSurface: () => ConsentBannerSurface;
33
- /**
34
- * Mirror a defaulted corner for right-to-left text so the banner keeps
35
- * hugging the reading start. Host-chosen positions and edge or center
36
- * placements are returned unchanged.
37
- *
38
- * @param position - The resolved position.
39
- * @param positionSource - Whether the host chose it.
40
- * @param variant - The resolved variant; only corners of floating and widget mirror.
41
- * @param textDirection - Document text direction.
42
- * @returns The position to render.
43
- * @internal
44
- */
45
- export declare const mirrorDefaultPosition: (position: PromptPosition, positionSource: 'host' | 'default', variant: PromptVariant, textDirection: 'ltr' | 'rtl') => PromptPosition;
@@ -4,13 +4,18 @@
4
4
  * @remarks
5
5
  * A notice prompt reads the `noticeTitle` and `noticeDescription` copy so
6
6
  * it informs rather than asks. Languages that have not translated the notice
7
- * copy fall back to the choice copy rather than to English.
7
+ * copy fall back to their own choice copy, then to English.
8
8
  *
9
9
  * @returns The copy the banner should render when no props override it.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * const { title, description } = useBannerCopy();
14
+ * ```
10
15
  * @public
11
16
  */
12
17
  export declare const useBannerCopy: () => {
13
- title: string | undefined;
14
- description: string | undefined;
18
+ title: string;
19
+ description: string;
15
20
  prompt: 'choice' | 'notice' | 'none';
16
21
  };
@@ -8,16 +8,53 @@ import type { ReactNode } from 'react';
8
8
  * @internal
9
9
  */
10
10
  export declare const readRecorded: <Value>(thenable: PromiseLike<Value>) => Value;
11
+ /** Props for {@link StreamedConsentSurface}. */
12
+ export interface StreamedConsentSurfaceProps {
13
+ /** The surface. Its consent hooks must run inside this component. */
14
+ children: ReactNode;
15
+ }
11
16
  /**
12
- * Renders a consent surface the server can stream. While the provider's
13
- * `prefetch` is a promise, the surface waits for it in its own Suspense
14
- * boundary: the rest of the page streams at once, and the surface follows
15
- * in a later chunk of the same response, rendered from the resolved
16
- * config. It shows only when that config carries a policy that shows it.
17
- * Hydration reads the same config, so the surface hydrates in place.
17
+ * Renders a consent surface the server can stream, such as a banner built
18
+ * with the headless hooks. The stock `ConsentBanner` uses it too.
18
19
  *
19
- * @internal
20
+ * While the provider's `prefetch` is a promise, as with Next.js
21
+ * `<ConsentRoot state={resolveConsent()}>`, the surface waits for it in its
22
+ * own Suspense boundary: the rest of the page streams at once, and the
23
+ * surface follows in a later chunk of the same response, rendered from the
24
+ * resolved config. It shows only when that config carries a policy that
25
+ * shows it. Hydration reads the same config, so the surface hydrates in
26
+ * place. Without a pending `prefetch`, it renders its children as they are.
27
+ *
28
+ * Call the hooks in a component inside it. Hooks called above it read the
29
+ * snapshot from before consent resolved, which has no banner.
30
+ *
31
+ * @param props - The surface to stream.
32
+ * @returns The children, in a Suspense boundary while consent streams.
33
+ * @example
34
+ * ```tsx
35
+ * 'use client';
36
+ * import {
37
+ * StreamedConsentSurface,
38
+ * useHeadlessConsentUI,
39
+ * } from 'c15t/next/headless';
40
+ *
41
+ * function BannerContent() {
42
+ * const { banner, performAction } = useHeadlessConsentUI();
43
+ * if (!banner.isVisible) return null;
44
+ * return (
45
+ * <section aria-label="Cookie consent">
46
+ * <button onClick={() => performAction('accept')}>Accept</button>
47
+ * </section>
48
+ * );
49
+ * }
50
+ *
51
+ * export function ConsentBanner() {
52
+ * return (
53
+ * <StreamedConsentSurface>
54
+ * <BannerContent />
55
+ * </StreamedConsentSurface>
56
+ * );
57
+ * }
58
+ * ```
20
59
  */
21
- export declare const StreamedSurface: ({ children }: {
22
- children: ReactNode;
23
- }) => string | number | bigint | boolean | React.JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | React.ReactPortal | null | undefined> | null | undefined;
60
+ export declare const StreamedConsentSurface: ({ children, }: StreamedConsentSurfaceProps) => string | number | bigint | boolean | React.JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | React.ReactPortal | null | undefined> | null | undefined;
@@ -4,10 +4,19 @@
4
4
  *
5
5
  * Pair with `ConsentProvider` from `@c15t/react` (or `@c15t/react/provider`).
6
6
  */
7
- export { defaultTranslationConfig, detectBrowserLanguage, mergeTranslationConfigs, prepareTranslationConfig, } from '@c15t/core';
7
+ export { defaultTranslationConfig, detectBrowserLanguage, mergeTranslationConfigs, prepareTranslationConfig, resolveActionLabels, resolvePreferenceControlLabels, resolvePromptCopy, } from '@c15t/core';
8
+ export type { PreferenceControl, PresentationAction, PromptCopy, ResolvedVendor, } from '@c15t/core';
9
+ export { type ConsentLabels, useConsentLabels, } from './component-hooks/use-consent-labels.js';
10
+ export { useConsentTextDirection } from './component-hooks/use-consent-text-direction.js';
11
+ export { type ConsentGateState, type UseConsentGateResult, useConsentGate, } from './component-hooks/use-consent-gate.js';
8
12
  export { type ConsentDialogTriggerVisibility, type UseConsentDialogTriggerOptions, type UseConsentDialogTriggerResult, useConsentDialogTrigger, } from './component-hooks/use-panel-trigger.js';
9
13
  export { type HeadlessConsentBannerAction, type HeadlessConsentBannerState, type HeadlessConsentDialogAction, type HeadlessConsentDialogState, type HeadlessConsentSurface, type HeadlessConsentSurfaceAction, type HeadlessConsentSurfaceState, type HeadlessConsentWriteAction, useHeadlessConsentUI, } from './component-hooks/use-headless-consent-ui.js';
10
14
  export { useTranslations } from './component-hooks/use-translations.js';
15
+ export { useVendorsListedUnder } from './component-hooks/use-vendors-listed-under.js';
16
+ export { useBannerCopy } from './components/prompt/use-banner-copy.js';
17
+ export { StreamedConsentSurface, type StreamedConsentSurfaceProps, } from './components/shared/streamed-surface.js';
11
18
  export { useColorScheme } from './hooks/use-color-scheme.js';
12
19
  export { useFocusTrap } from './hooks/use-focus-trap.js';
20
+ export { useIABPolicyGuard } from './hooks/use-iab-policy-guard.js';
21
+ export { useScrollLock } from './hooks/use-scroll-lock.js';
13
22
  export type { ReactUIOptions } from './types/manager.js';
@@ -28,8 +28,24 @@ export declare const missingIABProviderError: () => IABUnavailableError;
28
28
  * around them, so a missing IAB context alone does not prove the app has
29
29
  * none. The check also passes once `@c15t/react/iab` has loaded.
30
30
  *
31
+ * A custom banner or preference center that only handles opt-in and
32
+ * opt-out should call this too, and render nothing when `useModel()` is
33
+ * `'iab'`, as the standard surfaces do.
34
+ *
31
35
  * @throws {IABUnavailableError} When the policy needs IAB and neither an
32
36
  * IAB context nor `@c15t/react/iab` is present.
33
- * @internal
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * function Banner() {
41
+ * useIABPolicyGuard();
42
+ * const model = useModel();
43
+ * const { banner } = useHeadlessConsentUI();
44
+ * const copy = useBannerCopy();
45
+ * if (model === 'iab' || !banner.isVisible) return null;
46
+ * return <section>{copy.title}</section>;
47
+ * }
48
+ * ```
49
+ * @public
34
50
  */
35
51
  export declare const useIABPolicyGuard: () => void;
@@ -1,5 +1,19 @@
1
1
  /**
2
- * Hook to manage document scroll locking.
3
- * React wrapper for `@c15t/ui` setupScrollLock.
2
+ * Locks page scroll while `shouldLock` is true, as the stock banner does
3
+ * when its policy is blocking.
4
+ *
5
+ * @remarks
6
+ * Locks are counted, so a banner and a dialog can both hold one and the page
7
+ * scrolls again only when both release. Unmounting releases the lock.
8
+ *
9
+ * @param shouldLock - Whether the page should stop scrolling
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * const { banner } = useHeadlessConsentUI();
14
+ * useScrollLock(banner.isVisible && banner.blocking);
15
+ * ```
16
+ *
17
+ * @public
4
18
  */
5
19
  export declare const useScrollLock: (shouldLock: boolean) => void;
@@ -4,4 +4,4 @@
4
4
  * (esbuild) does not treat every module the index names as part of an app
5
5
  * that imports one hook: only the groups it uses are live.
6
6
  */
7
- export { offline } from '../transports/offline.js';
7
+ export { local, offline } from '../transports/offline.js';
@@ -22,5 +22,5 @@ export { hosted } from './transports/hosted.js';
22
22
  export type { HostedOptions } from './transports/hosted.js';
23
23
  export { manifest } from './transports/manifest.js';
24
24
  export type { BrowserManifestModeFactory, ManifestModeOptions, } from './transports/manifest.js';
25
- export { offline } from './transports/offline.js';
25
+ export { local, offline } from './transports/offline.js';
26
26
  export type { OfflineModeOptions } from './transports/offline.js';
@@ -1,6 +1,15 @@
1
1
  import type { Script, ScriptLoaderDebugEvent, ScriptLoaderHandle } from '@c15t/core/modules/script-loader';
2
2
  export interface UseScriptLoaderOptions {
3
3
  onDebug?: (event: ScriptLoaderDebugEvent) => void;
4
+ /**
5
+ * Set `false` to start every `scripts` entry after hydration. By default
6
+ * `ConsentRoot` in `c15t/next` and `c15t/tanstack-start` starts the
7
+ * entries the visitor's consent already allows from the server HTML,
8
+ * before the page hydrates.
9
+ *
10
+ * @default true
11
+ */
12
+ serverStart?: boolean;
4
13
  }
5
14
  export declare const useScriptLoader: (scripts: Script[], options?: UseScriptLoaderOptions) => ScriptLoaderHandle;
6
15
  export type { Script, ScriptLoaderDebugEvent, ScriptLoaderHandle };
@@ -66,20 +66,20 @@ export interface ConsentProviderOptions extends ConsentControlOptions, Pick<Reac
66
66
  /**
67
67
  * Transport factory the provider builds its kernel with. Required.
68
68
  *
69
- * Pass `hosted()` to talk to a c15t backend, `offline()` to resolve
69
+ * Pass `hosted()` to talk to a c15t backend, `local()` to resolve
70
70
  * policies locally with no network, or `custom()` to supply your own
71
71
  * kernel transport. This is an initial-only
72
72
  * option: remount the provider to change it.
73
73
  *
74
74
  * @example
75
75
  * ```tsx
76
- * import { ConsentProvider, hosted, offline } from '@c15t/react';
76
+ * import { ConsentProvider, hosted, local } from '@c15t/react';
77
77
  *
78
78
  * <ConsentProvider options={{ mode: hosted({ backendURL: '/api/c15t' }) }}>
79
79
  * {children}
80
80
  * </ConsentProvider>
81
81
  *
82
- * <ConsentProvider options={{ mode: offline() }}>{children}</ConsentProvider>
82
+ * <ConsentProvider options={{ mode: local() }}>{children}</ConsentProvider>
83
83
  * ```
84
84
  */
85
85
  mode: ProviderTransportFactory;
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Offline mode for the React provider: core's `offline()`, which resolves
2
+ * Local mode for the React provider: core's `local()`, which resolves
3
3
  * policy rules locally and switches copy when `overrides.language` or
4
4
  * `useSetLanguage()` names a language the bundle or `i18n.messages` has.
5
5
  * The language a server prefetch detected from `Accept-Language` keeps the
6
6
  * startup copy.
7
7
  */
8
- export { offline } from '@c15t/core';
8
+ export { local, offline } from '@c15t/core';
9
9
  export type { OfflineModeOptions } from '@c15t/core';
@@ -1 +1 @@
1
- export declare const version = "3.1.0";
1
+ export declare const version = "3.2.0-canary.0";
package/docs/README.md CHANGED
@@ -39,7 +39,7 @@ These docs describe c15t v3. Find your app's row in Choose your setup, then foll
39
39
  - [Customize](./frameworks/react/customize.md): Change the colors, fonts, layout, button styles and copy of the c15t banner and dialog in a React app with theme tokens, provider options and slots, and load the stylesheet yourself when you need to.
40
40
  - [Embeds](./frameworks/react/embeds.md): Keep YouTube videos, maps and other iframes out of a React page until their consent category is allowed, with ConsentGate or the iframe blocker in ConsentProvider.
41
41
  - [IAB GPP](./frameworks/react/gpp.md): Add the IAB Global Privacy Platform API (__gpp) to a React app with ConsentGPP from @c15t/react/gpp, so ad tech can read US state opt-outs and the TCF EU consent string.
42
- - [Headless](./frameworks/react/headless.md): Build a custom consent banner in React with the c15t/react/headless hooks inside your ConsentProvider.
42
+ - [Headless](./frameworks/react/headless.md): Build a custom consent banner or embed gate in React with the c15t/react/headless hooks inside your ConsentProvider.
43
43
  - [Hooks](./frameworks/react/hooks.md): Gate features and save consent choices in React components with the focused hooks exported from c15t/react.
44
44
  - [IAB TCF](./frameworks/react/iab.md): Mount the IAB TCF banner and dialog inside a React ConsentProvider and configure the CMP ID, policy and vendor data.
45
45
  - [Network blocker](./frameworks/react/network-blocker.md): Hold fetch and XMLHttpRequest calls in a React app until their consent category is allowed, with networkBlocker rules in the ConsentProvider options.
@@ -56,9 +56,9 @@ These docs describe c15t v3. Find your app's row in Choose your setup, then foll
56
56
  - [Choose your setup](./concepts/choose-your-setup.md): Pick the c15t setup for your framework, rendering mode and hosting, and decide who runs the consent backend.
57
57
  - [Consent categories](./concepts/consent-categories.md): Assign scripts, embeds and features to c15t consent categories, and understand which categories the preference dialog shows.
58
58
  - [Consent state reference](./concepts/consent-state.md): How c15t saves choices, gates IAB vendors, hydrates server records and keeps browser tabs and storage in step.
59
- - [Data fetching](./concepts/data-fetching.md): Choose the recommended build-time policy snapshot or runtime manifest fetching, backend /init, browser resolution or offline rules, and understand where consent choices are saved.
59
+ - [Data fetching](./concepts/data-fetching.md): Choose the recommended build-time policy snapshot or runtime manifest fetching, backend /init, browser resolution or local rules, and understand where consent choices are saved.
60
60
  - [How consent works](./concepts/how-consent-works.md): What c15t decides on each page load, the difference between a permission and a recorded choice, and what happens when a visitor saves.
61
- - [Consent modes](./concepts/modes.md): Reference for manifest(), hosted() and offline() in every c15t framework package, where each one resolves the visitor's policy, what it adds to first-load JavaScript, the build-time manifest fetch and its failure policy, and the backend URL variable each framework reads.
61
+ - [Consent modes](./concepts/modes.md): Reference for manifest(), hosted() and local() in every c15t framework package, where each one resolves the visitor's policy, what it adds to first-load JavaScript, the build-time manifest fetch and its failure policy, and the backend URL variable each framework reads.
62
62
  - [Policies](./concepts/policies.md): How policy models, prompts and scope decide what c15t asks visitors, where to change the rules, and why a banner may not appear.
63
63
 
64
64
  ## Guides
@@ -86,6 +86,7 @@ These docs describe c15t v3. Find your app's row in Choose your setup, then foll
86
86
  - [Ahrefs Analytics](./integrations/ahrefs-analytics.md): Load Ahrefs Web Analytics only after measurement consent with the c15t ahrefsAnalytics helper, and check it in DevTools.
87
87
  - [Amplitude](./integrations/amplitude.md): Load the Amplitude Browser SDK 2 only after measurement consent with the c15t amplitude helper, which opts the SDK out on revocation and back in on a new grant.
88
88
  - [Custom integrations](./integrations/building-integrations.md): Gate a vendor that has no @c15t/integrations helper, or sync consent with an SDK your app already loads, using a c15t script configuration.
89
+ - [Clay](./integrations/clay.md): Load Clay web intent only after marketing consent and understand its storage and revocation limits.
89
90
  - [Clearbit](./integrations/clearbit.md): Load the Clearbit enrichment tag only after marketing consent with the c15t clearbit helper, and check it in DevTools.
90
91
  - [Cloudflare Web Analytics](./integrations/cloudflare-web-analytics.md): Load the Cloudflare Web Analytics beacon only after measurement consent with the c15t cloudflareWebAnalytics helper, and check it in DevTools.
91
92
  - [Cloudflare Zaraz](./integrations/cloudflare-zaraz.md): Sync c15t permissions to Cloudflare Zaraz purposes with the c15t cloudflareZaraz bridge, which runs on every page without loading a script, and check the tools it controls.
@@ -110,7 +111,12 @@ These docs describe c15t v3. Find your app's row in Choose your setup, then foll
110
111
  - [Mixpanel](./integrations/mixpanel-analytics.md): Load the Mixpanel SDK on every page with the c15t mixpanelAnalytics helper, which switches Mixpanel tracking on and off with measurement consent, and check it in DevTools.
111
112
  - [OneDollarStats](./integrations/one-dollar-stats.md): Load the OneDollarStats tracker only after measurement consent with the c15t oneDollarStats helper, forward its data-attribute settings, and check it in DevTools.
112
113
  - [OpenAI Pixel](./integrations/openai-pixel.md): Load the ChatGPT Ads Measurement Pixel only after marketing consent with the c15t openaiPixel helper, guard oaiq conversion calls, and check it in DevTools.
113
- - [Integrations](./integrations/overview.md): Find all c15t integrations for analytics, tag managers, advertising, email and SMS, chat and embedded content.
114
+ - [Other ads and pixels](./integrations/other-ads-and-pixels.md): Consent-gated helpers for ad platforms and conversion pixels that don't need a full guide.
115
+ - [Other analytics tools](./integrations/other-analytics.md): Consent-gated helpers for analytics tools that don't need a full guide.
116
+ - [Other chat and support tools](./integrations/other-chat-and-support.md): Consent-gated helpers for chat and support tools that don't need a full guide.
117
+ - [Other email, SMS and marketing automation tools](./integrations/other-email-and-sms.md): Consent-gated helpers for email, SMS and marketing automation tools that don't need a full guide.
118
+ - [Experimentation and personalization tools](./integrations/other-experience.md): Consent-gated helpers for experimentation and personalization tools that don't need a full guide.
119
+ - [Integrations](./integrations/overview.md): Find all c15t integrations for analytics, tag managers, advertising, experimentation and personalization, email and SMS, chat and embedded content.
114
120
  - [Pinterest Tag](./integrations/pinterest-tag.md): Load the Pinterest Tag only after marketing consent with the c15t pinterestTag helper, guard pintrk event calls, and check it in DevTools.
115
121
  - [Pirsch](./integrations/pirsch.md): Load Pirsch Analytics only after measurement consent with the c15t pirsch helper, keep custom event bindings working, and check it in DevTools.
116
122
  - [Plausible Analytics](./integrations/plausible-analytics.md): Load the Plausible Analytics tracker only after measurement consent with the c15t plausibleAnalytics helper, and check it in DevTools.
@@ -121,6 +127,7 @@ These docs describe c15t v3. Find your app's row in Choose your setup, then foll
121
127
  - [Rybbit Analytics](./integrations/rybbit-analytics.md): Load Rybbit Analytics only after measurement consent with the c15t rybbitAnalytics helper, map its tracking options to data attributes, and check it in DevTools.
122
128
  - [Segment](./integrations/segment.md): Load Segment Analytics.js only after measurement consent with the c15t segment helper, guard your own track and identify calls, and check it in DevTools.
123
129
  - [Sentry](./integrations/sentry.md): Configure Sentry's CDN or your own SDK with consent for error monitoring, Session Replay and user data.
130
+ - [Shopify](./integrations/shopify.md): Send c15t consent to Shopify's Customer Privacy API from a Hydrogen storefront, so Shopify analytics wait for the visitor's choice and follow each later change.
124
131
  - [Snapchat Pixel](./integrations/snapchat-pixel.md): Load the Snapchat Pixel only after marketing consent with the c15t snapchatPixel helper, guard snaptr event calls, and check it in DevTools.
125
132
  - [TikTok Pixel](./integrations/tiktok-pixel.md): Load the TikTok Pixel only after marketing consent with the c15t tiktokPixel helper, guard ttq event calls, and check it in DevTools.
126
133
  - [Umami Analytics](./integrations/umami-analytics.md): Load the Umami Analytics tracker only after measurement consent with the c15t umamiAnalytics helper, point it at a self-hosted instance, and check it in DevTools.
@@ -45,11 +45,11 @@ and subscribe to it from your components.
45
45
  The backend supplies policies and stores consent records. Every setup above
46
46
  works with each option; only the URL changes.
47
47
 
48
- | Option | Use it when |
49
- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
50
- | [Inth](https://inth.com) | You want hosted policies and consent records. The guides use this. |
51
- | [Self-hosted c15t backend](https://c15t.com/docs/self-host/overview) | You need to run the service and its database yourself. |
52
- | Offline mode | Local development and tests. Policies live in your code and choices stay in the browser, with no consent records. Not recommended for production environments. |
48
+ | Option | Use it when |
49
+ | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
50
+ | [Inth](https://inth.com) | You want hosted policies and consent records. The guides use this. |
51
+ | [Self-hosted c15t backend](https://c15t.com/docs/self-host/overview) | You need to run the service and its database yourself. |
52
+ | [Local mode](./modes.md#local) | Local development and tests. Policies live in your code and choices stay in the browser. c15t keeps no server-side record of consent. Not recommended for production environments. |
53
53
 
54
54
  To use Inth, create a project, set its policy rules, add your site's origin to
55
55
  its trusted origins, and copy the backend URL. The guides use the placeholder
@@ -60,10 +60,10 @@ including any path prefix.
60
60
 
61
61
  Where consent resolves decides what the visitor's first HTML contains.
62
62
 
63
- | Rendering | What the visitor gets |
64
- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
65
- | The server resolves consent for each request | The server reads the visitor's cookie and location headers, so the browser starts with the answer. Gated scripts can run right after hydration, and the banner can be part of the HTML. |
66
- | The browser resolves consent | The HTML is the same for everyone. After the page loads, the browser requests the policy, then shows the banner or runs gated scripts. |
63
+ | Rendering | What the visitor gets |
64
+ | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
65
+ | The server resolves consent for each request | The server reads the visitor's cookie and location headers, so the browser starts with the answer. The banner can be part of the HTML. In Next.js, TanStack Start, Nuxt and SvelteKit, scripts the visitor's consent already allows start before hydration. |
66
+ | The browser resolves consent | The HTML is the same for everyone. After the page loads, the browser requests the policy, then shows the banner or runs gated scripts. |
67
67
 
68
68
  A static or prerendered page cannot contain one visitor's consent, because the
69
69
  same HTML goes to everyone. Static sites still use Inth; the browser talks to it
@@ -63,6 +63,23 @@ If your own scripts depend on such a category, do one of these:
63
63
  Check `effectivePermissions` after Accept All on a fresh visit to see which
64
64
  categories your vendor list leaves denied.
65
65
 
66
+ ## A subject ID from the first page load
67
+
68
+ Every visitor gets a subject ID (`sub_…`) before they decide anything, so
69
+ you can link activity to the visitor from the first page load and still
70
+ find it when they opt out later. It is in `snapshot.subject.subjectId` from
71
+ the first snapshot.
72
+
73
+ * A framework server sets the ID in the `c15t-sid` cookie on the first
74
+ response, where the adapter's server hook runs. Otherwise the browser
75
+ creates it when c15t starts and stores it in `c15t-sid` at once.
76
+ * The ID stays the same across reloads. The first save records the same ID
77
+ with the choice.
78
+ * `c15t-sid` is not a choice. A visitor with only an ID still sees the
79
+ banner.
80
+ * Clearing choices and revoking consent keep the ID. Only setting the subject
81
+ to `null` starts over with a new one.
82
+
66
83
  ## When a choice is saved
67
84
 
68
85
  A save records the choice in the browser first and sends it to the backend
@@ -204,10 +221,12 @@ Stored records are merged into the ones in memory:
204
221
  wins in both tabs.
205
222
  * Tabs keep one subject. A stored subject is considered only when the record
206
223
  that carries it changed, never on focus alone. It replaces a subject the tab
207
- generated on its first save or copied from storage, so a tab that opened
208
- before another tab stored a subject joins it. A subject id the server
209
- resolved (at init, from a prefetch or in a save response) is replaced only by
210
- a strictly newer stored choice, and an identity set with `identify()` is
224
+ generated at startup or on its first save, or copied from storage, so a tab
225
+ that opened before another tab stored a subject joins it. When it replaces
226
+ an ID the tab generated at startup and never used, the kernel emits
227
+ `subject:resolved` with `previousSubjectId`. A subject id the server
228
+ resolved (at init, from a prefetch or in a save response) is replaced only
229
+ by a strictly newer stored choice, and an identity set with `identify()` is
211
230
  never replaced.
212
231
  * Under an IAB policy, the TC string follows the reconciled choice. See
213
232
  [How the TC string is reconciled](#how-the-tc-string-is-reconciled).
@@ -281,11 +300,13 @@ wherever it turns up. A decision stamped in the very millisecond of the clear
281
300
  counts only if it comes from a tab that had already seen that clear, so a tab's
282
301
  own choice right after its own clear stands while another tab's decision in the
283
302
  same millisecond does not. A stored record left with no decisions after the
284
- clear is void too, subject included.
303
+ clear is void too, subject included. The subject ID itself survives the clear:
304
+ it lives in its own key, `c15t-sid`, which clearing never removes, so a
305
+ withdrawal stays linked to the same visitor.
285
306
 
286
307
  * A tab that reconciles only after another tab cleared and saved again drops
287
- its pre-clear decisions instead of merging them back. Its subject from before
288
- the clear is dropped too.
308
+ its pre-clear decisions instead of merging them back. The external identity
309
+ linked to its subject before the clear is dropped too; the subject ID stays.
289
310
  * A tab that missed the clear writes only decisions it made after it. Its queued
290
311
  write of an earlier decision is discarded, so it cannot bring back a cleared
291
312
  record.
@@ -348,13 +369,38 @@ When a server render seeded the page from the consent cookie
348
369
  localStorage is still applied on top of it when the page mounts, since it can
349
370
  only restrict, if it is newer than the seeded decision or from the same
350
371
  millisecond as a seeded grant. A stored grant is not.
372
+ `persistence: { skipHydration: false }` reads storage back over the seed
373
+ instead, so an older stored decision replaces it. A server render then starts
374
+ no scripts from the HTML, since it can't tell what that read will return.
351
375
 
352
376
  The notice dismissal and vendor denials are stored twice as well. A vendor
353
377
  list in localStorage at least as new as the cookie's adds its denials but never
354
378
  lifts one the cookie holds; a copy confirmed before the last clear is ignored, so its
355
379
  denials never come back. The newer notice dismissal applies; it still only hides a
356
- notice with the fingerprint it names. The consent record follows the rules
357
- below.
380
+ notice with the fingerprint it names.
381
+
382
+ Every notice acknowledgement also stores the visitor's subject ID with it,
383
+ including a choice that acknowledges a notice. For a visitor whose only action
384
+ was acknowledging a prompt that offers only necessary categories, this record
385
+ is what keeps the ID. The acknowledgement still grants no optional category. The `<key>-notice` cookie keeps its v1 format, so older
386
+ runtimes still read it. A separate `<key>-notice-subject` cookie binds the
387
+ subject ID to that acknowledgement: readers use it only while its timestamp and
388
+ fingerprint match the notice, so removing or replacing the notice makes a stale
389
+ identity cookie ineffective. When the subject ID changes, through a save
390
+ response or `set.subjectId()`, c15t rewrites the acknowledgement with the new
391
+ ID, so a reload does not bring the old one back. For the same acknowledgement,
392
+ the cookie identity wins unless a failed cookie write left a newer local
393
+ identity or a local identity removal. That exception lasts only while both
394
+ notice cookies match the values observed after the failed write; a later server
395
+ or sibling-subdomain cookie update takes precedence. Category-based data cleanup
396
+ protects these records and `<key>-sid` even when a configured wildcard matches
397
+ their names.
398
+
399
+ Clearing the stored records removes the acknowledgement and its
400
+ `<key>-notice-subject` cookie. The subject ID stays in `<key>-sid`, as after any
401
+ other clear. External identity fields supplied through `identify()` go with the
402
+ record that carried them. Call `set.subjectId(null)` to give the visitor a new
403
+ ID. The consent record follows the rules below.
358
404
 
359
405
  The consent record is stored twice: as a cookie, which a server render reads,
360
406
  and in localStorage. The cookie is authoritative. A well-formed cookie wins
@@ -385,6 +431,32 @@ render still reads the old consent cookie until the next successful cookie
385
431
  write. The same applies to a consent cookie set with a different `domain` than
386
432
  the current `storageConfig` uses, which the clear cannot remove.
387
433
 
434
+ The subject ID follows the same split. When its cookie writes succeed, the
435
+ server render and the browser use the same ID. When the browser refuses the
436
+ rewrite of a record cookie after the subject ID changed (the consent cookie,
437
+ `c15t-vendors` or `c15t-notice-subject`), that cookie keeps the old ID while
438
+ localStorage holds the new one. A server render then reports the old ID, and
439
+ the browser starts with the new ID from its storage. This happens even when the
440
+ request's `c15t-sid` cookie already holds the new ID. It lasts until a later
441
+ write of that record reaches the cookie, which may never happen while the
442
+ browser keeps refusing it. The server never replaces a `c15t-sid` cookie the
443
+ request already has with the old ID. When this browser's storage holds the
444
+ newer ID, the browser does not store the old one again either.
445
+
446
+ The vendor record has a separate limit. A local vendor copy at least as new as
447
+ the cookie's keeps its subject ID, and a rekey to a new subject ID keeps the
448
+ original decision time. So with `crossSubdomain`, a visitor whose only record is
449
+ a vendor decision (no consent record) can hold a local copy and a shared cookie
450
+ copy with the same time but different subject IDs, for example after another
451
+ subdomain rekeyed it at any later moment. This origin then starts with the ID
452
+ from its local copy, on server-rendered loads as well as loads that hydrate. It
453
+ does not write that ID over the shared `c15t-sid` cookie when that cookie and
454
+ the record cookies agree on another ID. A later vendor save from this origin
455
+ does store its ID in the shared vendor cookie, where the other subdomains read
456
+ it. Once that cookie carries it, this origin can also write it to the shared
457
+ `c15t-sid` cookie on a later page load. No optional category is granted and no
458
+ denial is lifted.
459
+
388
460
  ### Ordering with pending writes
389
461
 
390
462
  A tab writes its own choices in a later task, not during the click. Before it
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Data fetching
3
3
  description: Choose the recommended build-time policy snapshot or runtime
4
- manifest fetching, backend /init, browser resolution or offline rules, and
4
+ manifest fetching, backend /init, browser resolution or local rules, and
5
5
  understand where consent choices are saved.
6
6
  group: concepts
7
7
  ---
@@ -10,18 +10,18 @@ group: concepts
10
10
 
11
11
  Where policy resolves and where choices are saved are separate from who runs
12
12
  the backend. Inth and a [self-hosted backend](https://c15t.com/docs/self-host/quickstart) speak
13
- the same protocol, so every path below works with either. Only `offline()`
13
+ the same protocol, so every path below works with either. Only `local()`
14
14
  removes backend requests. For a recommendation by framework, start with
15
15
  [choose your setup](./choose-your-setup.md).
16
16
 
17
- | Fetching path | Where policy resolves | Where choices are submitted | Choose it when |
18
- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
19
- | Build-time manifest, recommended for server-rendered apps | Your server resolves a bundled policy snapshot with each visitor's inputs | Inth or your c15t backend | Your framework renders on the server, supports a build integration, and policy changes can ship with a rebuild |
20
- | Cached manifest on your server | Your application resolves public policy data with each request's location and signals | Inth or your c15t backend | Policy changes must reach the app without rebuilding, or builds cannot access the backend |
21
- | Regular backend `/init` | The consent backend | The same backend | You have no application server, as in a React, Vue or Svelte single-page app, want the fewest moving parts, or need backend-owned request resolution |
22
- | Manifest in the browser | The browser, using a supplied location, or `/init` when the policy needs one it doesn't have | Inth or your c15t backend | The policy has no location rules or the page knows the visitor's location, and you accept about 3 KB more bundle and a rebuild for policy edits |
23
- | Offline | The browser or local runtime, using bundled rules | No backend submission | Local development and tests. Not recommended for production environments. |
24
- | Custom transport | Your implementation | Your implementation | An existing service cannot use the c15t backend protocol |
17
+ | Fetching path | Where policy resolves | Where choices are submitted | Choose it when |
18
+ | --------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
19
+ | Build-time manifest, recommended for server-rendered apps | Your server resolves a bundled policy snapshot with each visitor's inputs | Inth or your c15t backend | Your framework renders on the server, supports a build integration, and policy changes can ship with a rebuild |
20
+ | Cached manifest on your server | Your application resolves public policy data with each request's location and signals | Inth or your c15t backend | Policy changes must reach the app without rebuilding, or builds cannot access the backend |
21
+ | Regular backend `/init` | The consent backend | The same backend | You have no application server, as in a React, Vue or Svelte single-page app, want the fewest moving parts, or need backend-owned request resolution |
22
+ | Manifest in the browser | The browser, using a supplied location, or `/init` when the policy needs one it doesn't have | Inth or your c15t backend | The policy has no location rules or the page knows the visitor's location, and you accept about 3 KB more bundle and a rebuild for policy edits |
23
+ | Local | The browser or local runtime, using bundled rules | No backend submission. c15t keeps no server-side record of consent. | Local development and tests. Not recommended for production environments. |
24
+ | Custom transport | Your implementation | Your implementation | An existing service cannot use the c15t backend protocol |
25
25
 
26
26
  ## What is a consent manifest?
27
27
 
@@ -226,36 +226,43 @@ needing the upstream URL. A static export cannot serve a Next.js route or
226
226
  rewrite at runtime; use the absolute Inth URL or a proxy provided by the static
227
227
  host instead.
228
228
 
229
- ## When should I use offline mode?
229
+ ## When should I use local mode?
230
230
 
231
231
  Not recommended for production environments. Use Inth or a self-hosted backend
232
232
  for production policy and consent records.
233
233
 
234
- `offline()` resolves bundled policy rules without an init request and acknowledges
235
- saves locally. The runtime's persistence module stores the choice in browser
236
- storage. There is no backend audit history, cross-device record service or IP
237
- geolocation supplied by this transport.
234
+ `local()`, called `offline()` before c15t 3.2, resolves bundled policy rules
235
+ without an init request and acknowledges saves locally. The runtime's
236
+ persistence module stores the choice in browser storage. c15t keeps no
237
+ server-side record of consent, so there is no audit history, no cross-device
238
+ record and no IP geolocation. [What a consent record proves](./modes.md#what-a-consent-record-proves)
239
+ explains what that leaves out.
238
240
 
239
- Use your framework's `offline()` so its translations and provider context are
241
+ Use your framework's `local()` so its translations and provider context are
240
242
  included. In a React app it replaces the mode passed to `ConsentProvider`:
241
243
 
242
244
  ```ts title="src/consent-mode.ts"
243
- import { offline } from 'c15t/react';
245
+ import { local } from 'c15t/react';
244
246
 
245
- export const mode = offline();
247
+ export const mode = local();
246
248
  ```
247
249
 
250
+ In development builds, local mode warns once per page that c15t keeps no
251
+ server-side record of consent. Pass `local({ acknowledgeNoRecords: true })` to
252
+ hide the warning once you have decided a browser-only record is enough; see
253
+ [the development warning](./modes.md#the-development-warning).
254
+
248
255
  In Next.js, TanStack Start, Nuxt, Astro and SvelteKit, pass the data
249
- `offline()` from the framework package where the config takes `mode`; see
256
+ `local()` from the framework package where the config takes `mode`; see
250
257
  [consent modes](./modes.md#import-the-factories-from-your-framework-package).
251
258
 
252
- With no `policyRules`, the current offline transport uses the recommended rule
253
- pack. Supplying `policyRules` replaces that pack. Unknown country and region are
254
- real resolution inputs; offline mode does not discover a visitor's location.
259
+ With no `policyRules`, the local transport uses the recommended rule pack.
260
+ Supplying `policyRules` replaces that pack. Unknown country and region are
261
+ real resolution inputs; local mode does not discover a visitor's location.
255
262
  Use [policy rules](./policies.md) to understand
256
263
  matching and defaults, and test the missing-location case.
257
264
 
258
- Offline mode is an explicit architecture choice, not an automatic fallback for
265
+ Local mode is an explicit architecture choice, not an automatic fallback for
259
266
  a failed Inth request. If hosted initialization fails before a policy resolves,
260
267
  optional permissions remain denied and the stock prompt stays hidden. Observe
261
268
  initialization failures instead of silently changing policy sources.