@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.
- package/AGENTS.md +11 -4
- package/README.md +2 -2
- package/dist/component-hooks/use-consent-gate.js +2 -0
- package/dist/component-hooks/use-consent-labels.js +2 -0
- package/dist/component-hooks/use-consent-text-direction.js +2 -0
- package/dist/component-hooks/use-vendors-listed-under.js +2 -0
- package/dist/components/consent-gate/atoms.js +1 -1
- package/dist/components/consent-gate/consent-gate.js +1 -1
- package/dist/components/iab-prompt/atoms/root.js +1 -1
- package/dist/components/panel-trigger/atoms/toolbar.js +1 -1
- package/dist/components/preferences/atoms/button.js +1 -1
- package/dist/components/preferences/atoms/vendor-list.js +1 -1
- package/dist/components/prompt/atoms/root.js +1 -1
- package/dist/components/prompt/components.js +1 -1
- package/dist/components/prompt/policy-actions.js +1 -1
- package/dist/components/prompt/prompt.js +1 -1
- package/dist/components/prompt/surface-context.js +1 -1
- package/dist/components/prompt/use-banner-copy.js +1 -1
- package/dist/components/shared/streamed-surface.js +1 -1
- package/dist/headless.js +1 -1
- package/dist/index-parts/offline.js +1 -1
- package/dist/modes.js +1 -1
- package/dist/transports/offline.js +1 -1
- package/dist/version.js +1 -1
- package/dist-types/component-hooks/use-consent-gate.d.ts +102 -0
- package/dist-types/component-hooks/use-consent-labels.d.ts +32 -0
- package/dist-types/component-hooks/use-consent-text-direction.d.ts +22 -0
- package/dist-types/component-hooks/use-vendors-listed-under.d.ts +30 -0
- package/dist-types/components/prompt/atoms/root.d.ts +3 -3
- package/dist-types/components/prompt/prompt.d.ts +4 -3
- package/dist-types/components/prompt/surface-context.d.ts +1 -14
- package/dist-types/components/prompt/use-banner-copy.d.ts +8 -3
- package/dist-types/components/shared/streamed-surface.d.ts +47 -10
- package/dist-types/headless.d.ts +10 -1
- package/dist-types/hooks/use-iab-policy-guard.d.ts +17 -1
- package/dist-types/hooks/use-scroll-lock.d.ts +16 -2
- package/dist-types/index-parts/offline.d.ts +1 -1
- package/dist-types/modes.d.ts +1 -1
- package/dist-types/module-hooks/script-loader.d.ts +9 -0
- package/dist-types/provider.d.ts +3 -3
- package/dist-types/transports/offline.d.ts +2 -2
- package/dist-types/version.d.ts +1 -1
- package/docs/README.md +11 -4
- package/docs/concepts/choose-your-setup.md +9 -9
- package/docs/concepts/consent-state.md +81 -9
- package/docs/concepts/data-fetching.md +30 -23
- package/docs/concepts/modes.md +85 -35
- package/docs/customization/recipes.md +4 -4
- package/docs/customization/stylesheets.md +3 -3
- package/docs/customization/tailwind.md +17 -34
- package/docs/customization/translations.md +18 -12
- package/docs/frameworks/react/clear-on-revocation.md +3 -2
- package/docs/frameworks/react/components/consent-banner.md +3 -3
- package/docs/frameworks/react/components/consent-provider.md +29 -29
- package/docs/frameworks/react/gpp.md +1 -1
- package/docs/frameworks/react/headless.md +146 -19
- package/docs/frameworks/react/iab.md +8 -1
- package/docs/frameworks/react/quickstart.md +1 -1
- package/docs/frameworks/react/rendering.md +1 -1
- package/docs/frameworks/react/translations.md +9 -9
- package/docs/frameworks/react/troubleshooting.md +1 -1
- package/docs/frameworks/react/upgrade-v3.md +39 -32
- package/docs/frameworks/react/vendor-consent.md +5 -0
- package/docs/guides/banner-experiments.md +3 -3
- package/docs/guides/troubleshooting.md +3 -2
- package/docs/integrations/adobe-analytics.md +2 -2
- package/docs/integrations/ahrefs-analytics.md +2 -2
- package/docs/integrations/amplitude.md +2 -2
- package/docs/integrations/clay.md +305 -0
- package/docs/integrations/clearbit.md +2 -2
- package/docs/integrations/cloudflare-web-analytics.md +2 -2
- package/docs/integrations/cloudflare-zaraz.md +2 -2
- package/docs/integrations/crisp.md +2 -2
- package/docs/integrations/databuddy.md +2 -2
- package/docs/integrations/fathom-analytics.md +2 -2
- package/docs/integrations/front-chat.md +2 -2
- package/docs/integrations/google-maps.md +2 -2
- package/docs/integrations/google-tag-manager.md +59 -2
- package/docs/integrations/google-tag.md +2 -2
- package/docs/integrations/heap.md +2 -2
- package/docs/integrations/hightouch.md +2 -2
- package/docs/integrations/hotjar.md +40 -26
- package/docs/integrations/intercom.md +2 -2
- package/docs/integrations/klaviyo.md +2 -2
- package/docs/integrations/linkedin-insights.md +2 -2
- package/docs/integrations/logrocket.md +2 -2
- package/docs/integrations/matomo-analytics.md +2 -2
- package/docs/integrations/meta-pixel.md +2 -2
- package/docs/integrations/microsoft-clarity.md +2 -2
- package/docs/integrations/microsoft-uet.md +2 -2
- package/docs/integrations/mixpanel-analytics.md +2 -2
- package/docs/integrations/one-dollar-stats.md +2 -2
- package/docs/integrations/openai-pixel.md +2 -2
- package/docs/integrations/other-ads-and-pixels.md +94 -0
- package/docs/integrations/other-analytics.md +108 -0
- package/docs/integrations/other-chat-and-support.md +112 -0
- package/docs/integrations/other-email-and-sms.md +156 -0
- package/docs/integrations/other-experience.md +222 -0
- package/docs/integrations/overview.md +55 -21
- package/docs/integrations/pinterest-tag.md +2 -2
- package/docs/integrations/pirsch.md +2 -2
- package/docs/integrations/plausible-analytics.md +2 -2
- package/docs/integrations/posthog.md +2 -2
- package/docs/integrations/promptwatch.md +2 -2
- package/docs/integrations/reddit-pixel.md +2 -2
- package/docs/integrations/rudderstack.md +2 -2
- package/docs/integrations/rybbit-analytics.md +2 -2
- package/docs/integrations/segment.md +2 -2
- package/docs/integrations/sentry.md +2 -2
- package/docs/integrations/shopify.md +323 -0
- package/docs/integrations/snapchat-pixel.md +2 -2
- package/docs/integrations/tiktok-pixel.md +2 -2
- package/docs/integrations/umami-analytics.md +2 -2
- package/docs/integrations/vercel-analytics.md +2 -2
- package/docs/integrations/vercel-speed-insights.md +7 -2
- package/docs/integrations/x-pixel.md +2 -2
- package/docs/integrations/youtube.md +2 -2
- package/package.json +10 -32
- package/dist/components/prompt/resolve-banner-primary-actions.js +0 -1
- package/dist-types/components/prompt/resolve-banner-primary-actions.d.ts +0 -14
|
@@ -173,7 +173,7 @@ pass them as a top-level prop:
|
|
|
173
173
|
|
|
174
174
|
```svelte title="src/App.svelte"
|
|
175
175
|
<script lang="ts">
|
|
176
|
-
import { ConsentProvider, manifest } from '
|
|
176
|
+
import { ConsentProvider, manifest } from 'c15t/svelte';
|
|
177
177
|
import { scripts } from './consent-scripts';
|
|
178
178
|
</script>
|
|
179
179
|
|
|
@@ -192,7 +192,7 @@ handle and the layout load from the [SvelteKit quickstart](https://c15t.com/docs
|
|
|
192
192
|
|
|
193
193
|
```svelte title="src/routes/+layout.svelte"
|
|
194
194
|
<script lang="ts">
|
|
195
|
-
import { ConsentRoot } from '
|
|
195
|
+
import { ConsentRoot } from 'c15t/svelte';
|
|
196
196
|
import { scripts } from '../consent-scripts';
|
|
197
197
|
|
|
198
198
|
let { children, data } = $props();
|
|
@@ -165,7 +165,7 @@ pass them as a top-level prop:
|
|
|
165
165
|
|
|
166
166
|
```svelte title="src/App.svelte"
|
|
167
167
|
<script lang="ts">
|
|
168
|
-
import { ConsentProvider, manifest } from '
|
|
168
|
+
import { ConsentProvider, manifest } from 'c15t/svelte';
|
|
169
169
|
import { scripts } from './consent-scripts';
|
|
170
170
|
</script>
|
|
171
171
|
|
|
@@ -184,7 +184,7 @@ handle and the layout load from the [SvelteKit quickstart](https://c15t.com/docs
|
|
|
184
184
|
|
|
185
185
|
```svelte title="src/routes/+layout.svelte"
|
|
186
186
|
<script lang="ts">
|
|
187
|
-
import { ConsentRoot } from '
|
|
187
|
+
import { ConsentRoot } from 'c15t/svelte';
|
|
188
188
|
import { scripts } from '../consent-scripts';
|
|
189
189
|
|
|
190
190
|
let { children, data } = $props();
|
|
@@ -166,7 +166,7 @@ pass them as a top-level prop:
|
|
|
166
166
|
|
|
167
167
|
```svelte title="src/App.svelte"
|
|
168
168
|
<script lang="ts">
|
|
169
|
-
import { ConsentProvider, manifest } from '
|
|
169
|
+
import { ConsentProvider, manifest } from 'c15t/svelte';
|
|
170
170
|
import { scripts } from './consent-scripts';
|
|
171
171
|
</script>
|
|
172
172
|
|
|
@@ -185,7 +185,7 @@ handle and the layout load from the [SvelteKit quickstart](https://c15t.com/docs
|
|
|
185
185
|
|
|
186
186
|
```svelte title="src/routes/+layout.svelte"
|
|
187
187
|
<script lang="ts">
|
|
188
|
-
import { ConsentRoot } from '
|
|
188
|
+
import { ConsentRoot } from 'c15t/svelte';
|
|
189
189
|
import { scripts } from '../consent-scripts';
|
|
190
190
|
|
|
191
191
|
let { children, data } = $props();
|
|
@@ -184,7 +184,7 @@ pass them as a top-level prop:
|
|
|
184
184
|
|
|
185
185
|
```svelte title="src/App.svelte"
|
|
186
186
|
<script lang="ts">
|
|
187
|
-
import { ConsentProvider, manifest } from '
|
|
187
|
+
import { ConsentProvider, manifest } from 'c15t/svelte';
|
|
188
188
|
import { scripts } from './consent-scripts';
|
|
189
189
|
</script>
|
|
190
190
|
|
|
@@ -203,7 +203,7 @@ handle and the layout load from the [SvelteKit quickstart](https://c15t.com/docs
|
|
|
203
203
|
|
|
204
204
|
```svelte title="src/routes/+layout.svelte"
|
|
205
205
|
<script lang="ts">
|
|
206
|
-
import { ConsentRoot } from '
|
|
206
|
+
import { ConsentRoot } from 'c15t/svelte';
|
|
207
207
|
import { scripts } from '../consent-scripts';
|
|
208
208
|
|
|
209
209
|
let { children, data } = $props();
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Shopify
|
|
3
|
+
description: Send c15t consent to Shopify's Customer Privacy API from a Hydrogen
|
|
4
|
+
storefront, so Shopify analytics wait for the visitor's choice and follow each
|
|
5
|
+
later change.
|
|
6
|
+
seoTitle: Shopify Hydrogen cookie consent with c15t
|
|
7
|
+
seoDescription: Use your c15t banner as a custom consent banner in Shopify
|
|
8
|
+
Hydrogen. The c15t Shopify bridge passes each choice to Shopify's Customer
|
|
9
|
+
Privacy API.
|
|
10
|
+
group: integrations
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
The c15t Shopify bridge sends each consent choice to Shopify's Customer
|
|
14
|
+
Privacy API. Shopify's analytics, and the apps and pixels that read that API,
|
|
15
|
+
then follow your c15t banner instead of Shopify's own. The bridge loads no
|
|
16
|
+
script. Hydrogen's `ShopifyScripts` loads the Customer Privacy API, and the
|
|
17
|
+
bridge's `setup` function holds Hydrogen's analytics events until Shopify has
|
|
18
|
+
the visitor's choice.
|
|
19
|
+
|
|
20
|
+
The [Shopify example](https://github.com/c15t/c15t/tree/v3/examples/shopify-nextjs)
|
|
21
|
+
runs the code on this page in a Next.js app against mock.shop.
|
|
22
|
+
|
|
23
|
+
## Add the bridge
|
|
24
|
+
|
|
25
|
+
Set up c15t with the [Next.js quickstart](https://c15t.com/docs/frameworks/next/quickstart)
|
|
26
|
+
first, then install the integrations package:
|
|
27
|
+
|
|
28
|
+
| Package manager | Command |
|
|
29
|
+
| :-------------- | :------------------------------- |
|
|
30
|
+
| npm | `npm install @c15t/integrations` |
|
|
31
|
+
| pnpm | `pnpm add @c15t/integrations` |
|
|
32
|
+
| yarn | `yarn add @c15t/integrations` |
|
|
33
|
+
| bun | `bun add @c15t/integrations` |
|
|
34
|
+
|
|
35
|
+
Create the bridge once per page and pass its `setup` to `ShopifyScripts` with
|
|
36
|
+
`mode: 'custom-banner'`. A second component feeds the bridge each c15t
|
|
37
|
+
snapshot:
|
|
38
|
+
|
|
39
|
+
```tsx title="components/shopify-analytics.tsx"
|
|
40
|
+
'use client';
|
|
41
|
+
|
|
42
|
+
import { createShopifyCustomerPrivacy } from '@c15t/integrations/shopify';
|
|
43
|
+
import type { ShopifyScriptsI18n, ShopifyScriptsShop } from '@shopify/hydrogen';
|
|
44
|
+
import { ShopifyScripts } from '@shopify/hydrogen/react';
|
|
45
|
+
import { useSnapshot } from 'c15t/next';
|
|
46
|
+
import { useRouter } from 'next/navigation';
|
|
47
|
+
import { useEffect } from 'react';
|
|
48
|
+
|
|
49
|
+
// One bridge per page. It sends each c15t choice to Shopify's Customer
|
|
50
|
+
// Privacy API, and its `setup` holds Hydrogen's analytics until Shopify has
|
|
51
|
+
// the visitor's choice.
|
|
52
|
+
const shopifyPrivacy = createShopifyCustomerPrivacy();
|
|
53
|
+
|
|
54
|
+
/** Hydrogen's scripts and analytics bus. Render it in the document head. */
|
|
55
|
+
export const ShopifyAnalytics = ({
|
|
56
|
+
i18n,
|
|
57
|
+
shop,
|
|
58
|
+
}: {
|
|
59
|
+
i18n: ShopifyScriptsI18n;
|
|
60
|
+
shop: ShopifyScriptsShop;
|
|
61
|
+
}) => {
|
|
62
|
+
const router = useRouter();
|
|
63
|
+
return (
|
|
64
|
+
<ShopifyScripts
|
|
65
|
+
analytics={{ channel: 'headless' }}
|
|
66
|
+
consent={{ mode: 'custom-banner', setup: shopifyPrivacy.setup }}
|
|
67
|
+
i18n={i18n}
|
|
68
|
+
// Shopify's scripts route storefront links through Next's router.
|
|
69
|
+
navigate={(url) => router.push(url)}
|
|
70
|
+
shop={shop}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Feeds each c15t snapshot to the bridge. Render it inside `ConsentRoot`. */
|
|
76
|
+
export const ShopifyConsentSync = () => {
|
|
77
|
+
const snapshot = useSnapshot();
|
|
78
|
+
useEffect(() => shopifyPrivacy.update(snapshot), [snapshot]);
|
|
79
|
+
return null;
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Render `ShopifyAnalytics` in the document head, as Hydrogen requires, with
|
|
84
|
+
your shop's details. Render `ShopifyConsentSync` inside `ConsentRoot`:
|
|
85
|
+
|
|
86
|
+
```tsx title="app/layout.tsx"
|
|
87
|
+
import {
|
|
88
|
+
ConsentBanner,
|
|
89
|
+
ConsentDialog,
|
|
90
|
+
ConsentDialogLink,
|
|
91
|
+
ConsentRoot,
|
|
92
|
+
} from 'c15t/next';
|
|
93
|
+
import { resolveConsent } from 'c15t/next/server';
|
|
94
|
+
import { connection } from 'next/server';
|
|
95
|
+
import { Suspense } from 'react';
|
|
96
|
+
import type { ReactNode } from 'react';
|
|
97
|
+
|
|
98
|
+
import { PageViewTracker } from '@/components/page-view-tracker';
|
|
99
|
+
import {
|
|
100
|
+
ShopifyAnalytics,
|
|
101
|
+
ShopifyConsentSync,
|
|
102
|
+
} from '@/components/shopify-analytics';
|
|
103
|
+
import { SiteHeader } from '@/components/site-header';
|
|
104
|
+
import { StoreProviders } from '@/components/store-providers';
|
|
105
|
+
import { currency, i18n, shop } from '@/lib/shop';
|
|
106
|
+
import { getCart } from '@/lib/storefront';
|
|
107
|
+
|
|
108
|
+
import './globals.css';
|
|
109
|
+
|
|
110
|
+
const RootLayout = async ({ children }: { children: ReactNode }) => {
|
|
111
|
+
// The cart comes from the visitor's cookie, so every page renders per request.
|
|
112
|
+
await connection();
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<html lang="en">
|
|
116
|
+
<head>
|
|
117
|
+
{/* Hydrogen's scripts belong in the head, once per page. */}
|
|
118
|
+
<ShopifyAnalytics i18n={{ ...i18n, currency }} shop={shop} />
|
|
119
|
+
</head>
|
|
120
|
+
<body>
|
|
121
|
+
<ConsentRoot state={resolveConsent()}>
|
|
122
|
+
<ShopifyConsentSync />
|
|
123
|
+
<StoreProviders cart={getCart()}>
|
|
124
|
+
<SiteHeader />
|
|
125
|
+
<Suspense fallback={null}>
|
|
126
|
+
<PageViewTracker />
|
|
127
|
+
</Suspense>
|
|
128
|
+
{children}
|
|
129
|
+
</StoreProviders>
|
|
130
|
+
<ConsentBanner />
|
|
131
|
+
<ConsentDialog />
|
|
132
|
+
<footer className="site-footer">
|
|
133
|
+
<ConsentDialogLink>Privacy settings</ConsentDialogLink>
|
|
134
|
+
</footer>
|
|
135
|
+
</ConsentRoot>
|
|
136
|
+
</body>
|
|
137
|
+
</html>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export default RootLayout;
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Declare the categories
|
|
145
|
+
|
|
146
|
+
c15t's dialog offers only the categories that something on the page uses.
|
|
147
|
+
Shopify's scripts load through `ShopifyScripts`, not through c15t's `scripts`
|
|
148
|
+
option, so c15t cannot find them. Declare the categories the bridge maps, or
|
|
149
|
+
the dialog shows only Necessary and Shopify receives a denial for everything:
|
|
150
|
+
|
|
151
|
+
```ts title="c15t.config.ts"
|
|
152
|
+
import { defineConsentConfig } from 'c15t/next';
|
|
153
|
+
|
|
154
|
+
export default defineConsentConfig({
|
|
155
|
+
options: {
|
|
156
|
+
// The categories the Shopify bridge maps. Shopify's scripts load through
|
|
157
|
+
// Hydrogen's `ShopifyScripts`, not `scripts`, so c15t can't discover them.
|
|
158
|
+
consentCategories: ['measurement', 'marketing', 'functionality'],
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The categories must also be in your policy's scope.
|
|
164
|
+
|
|
165
|
+
## Serve the Storefront API route
|
|
166
|
+
|
|
167
|
+
The Customer Privacy API saves each choice by posting to the storefront's own
|
|
168
|
+
`/api/<version>/graphql.json`. Hydrogen's `handleShopifyRoutes` serves that
|
|
169
|
+
route, forwards it to your shop and keeps the cookies Shopify sets. In
|
|
170
|
+
Next.js, call it from `proxy.ts`:
|
|
171
|
+
|
|
172
|
+
```ts title="proxy.ts"
|
|
173
|
+
import {
|
|
174
|
+
createShopifyRequestContext,
|
|
175
|
+
createStorefrontClient,
|
|
176
|
+
handleShopifyRoutes,
|
|
177
|
+
} from '@shopify/hydrogen';
|
|
178
|
+
import { NextResponse } from 'next/server';
|
|
179
|
+
import type { NextRequest } from 'next/server';
|
|
180
|
+
|
|
181
|
+
import { cartHandlers } from '@/lib/cart-handlers';
|
|
182
|
+
import { i18n, storeDomain } from '@/lib/shop';
|
|
183
|
+
|
|
184
|
+
// This store has no customer accounts, so nothing keeps a session.
|
|
185
|
+
const noSession = {
|
|
186
|
+
getSessionItem: () => undefined,
|
|
187
|
+
getSessionOrigin: () => '',
|
|
188
|
+
removeSessionItem: () => undefined,
|
|
189
|
+
setSessionItem: () => undefined,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Serves Hydrogen's routes: `/api/cart` for the cart, and the Storefront API
|
|
194
|
+
* at `/api/<version>/graphql.json`, where Shopify's Customer Privacy API
|
|
195
|
+
* saves each consent choice.
|
|
196
|
+
*/
|
|
197
|
+
export const proxy = (request: NextRequest) => {
|
|
198
|
+
const requestContext = createShopifyRequestContext({ i18n, request });
|
|
199
|
+
const storefrontClient = createStorefrontClient({
|
|
200
|
+
config: { publicStorefrontToken: undefined, storeDomain },
|
|
201
|
+
requestContext,
|
|
202
|
+
type: 'public',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const shopifyRoute = handleShopifyRoutes({
|
|
206
|
+
handlers: [cartHandlers],
|
|
207
|
+
request,
|
|
208
|
+
requestContext,
|
|
209
|
+
sessionManager: noSession,
|
|
210
|
+
storefrontClient,
|
|
211
|
+
});
|
|
212
|
+
if (shopifyRoute) {
|
|
213
|
+
return shopifyRoute;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const response = NextResponse.next({
|
|
217
|
+
request: { headers: requestContext.getForwardedRequestHeaders() },
|
|
218
|
+
});
|
|
219
|
+
requestContext.applyResponseHeaders(response.headers);
|
|
220
|
+
return response;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export const config = {
|
|
224
|
+
matcher: ['/((?!_next/static|_next/image|_next/data|favicon.ico).*)'],
|
|
225
|
+
};
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The consent route needs no handlers. The example adds Hydrogen's cart
|
|
229
|
+
handlers for its cart, and its `lib/shop.ts` holds the store domain and
|
|
230
|
+
market. It has no customer accounts, so it passes a session manager that
|
|
231
|
+
stores nothing. Pass your own if your storefront signs customers in.
|
|
232
|
+
|
|
233
|
+
## How categories map
|
|
234
|
+
|
|
235
|
+
| Shopify consent | c15t category |
|
|
236
|
+
| --------------- | --------------- |
|
|
237
|
+
| `analytics` | `measurement` |
|
|
238
|
+
| `marketing` | `marketing` |
|
|
239
|
+
| `preferences` | `functionality` |
|
|
240
|
+
| `sale_of_data` | `marketing` |
|
|
241
|
+
|
|
242
|
+
Each Shopify value is `true` only while its c15t category is allowed in the
|
|
243
|
+
effective permissions, which include policy restrictions. Pass `categories`
|
|
244
|
+
to change the mapping:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
const shopifyPrivacy = createShopifyCustomerPrivacy({
|
|
248
|
+
categories: { sale_of_data: 'measurement' },
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`toShopifyConsent(permissions, categories)` returns the same four values for
|
|
253
|
+
your own code.
|
|
254
|
+
|
|
255
|
+
## When the bridge sends
|
|
256
|
+
|
|
257
|
+
Shopify records consent only when the visitor makes a choice, so the bridge
|
|
258
|
+
sends nothing until then, or while the policy is pending.
|
|
259
|
+
|
|
260
|
+
* **Opt-in or IAB policy:** `setup` waits until the visitor answers the
|
|
261
|
+
banner, including with Reject All, so Hydrogen holds events until then.
|
|
262
|
+
* **Opt-out policy:** `setup` resolves without recording a choice, and
|
|
263
|
+
Shopify applies its region defaults, including Global Privacy Control,
|
|
264
|
+
until the visitor saves a choice in c15t. A stale grant is the exception,
|
|
265
|
+
below.
|
|
266
|
+
|
|
267
|
+
After the first choice, the bridge sends each change once. When a visitor
|
|
268
|
+
revokes a category, c15t reloads the page and the bridge sends the stored
|
|
269
|
+
choice on load.
|
|
270
|
+
|
|
271
|
+
While the visitor has no current choice, Shopify may still hold grants from
|
|
272
|
+
an earlier one, for example after that choice expired or a policy change
|
|
273
|
+
asks again. If c15t now denies any of them, the bridge sends `false` for
|
|
274
|
+
those grants only, so Shopify keeps the other signals, including unset ones.
|
|
275
|
+
It counts grants the bridge sent and each `yes` from Shopify's
|
|
276
|
+
`currentVisitorConsent()`, and sends the revocation again if Shopify reports
|
|
277
|
+
a dropped grant again, for example after another tab granted it. If that
|
|
278
|
+
function isn't available, it assumes a choice c15t stored may have granted
|
|
279
|
+
anything. Under an opt-in or IAB policy, Hydrogen keeps holding events until
|
|
280
|
+
the visitor answers. Under opt-out, `setup` resolves once Shopify accepts the
|
|
281
|
+
revocation.
|
|
282
|
+
|
|
283
|
+
If the Customer Privacy API hasn't loaded yet, the bridge waits for
|
|
284
|
+
Shopify's `consentTrackingApiLoaded` or `shopifyCustomerPrivacyApiLoaded`
|
|
285
|
+
event.
|
|
286
|
+
|
|
287
|
+
`setup` resolves once Shopify has accepted the visitor's latest choice, and
|
|
288
|
+
Hydrogen then delivers the events that choice allows. If the visitor changes
|
|
289
|
+
their mind while the first request is in flight, `setup` waits for the newer
|
|
290
|
+
one. It never rejects, because Hydrogen stops calling `setup` after a
|
|
291
|
+
rejection. When Shopify rejects a choice, the bridge keeps events blocked and
|
|
292
|
+
sends it again after 1, 2 and 4 seconds, then on the next update. If the
|
|
293
|
+
policy turns pending meanwhile, the bridge drops those retries.
|
|
294
|
+
|
|
295
|
+
## Options
|
|
296
|
+
|
|
297
|
+
| Option | Default | Behavior |
|
|
298
|
+
| ------------ | ------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
299
|
+
| `categories` | The table above | Overrides the c15t category for any of the four Shopify values. |
|
|
300
|
+
| `onSync` | None | Called with what Shopify accepted: the four values for a choice, or the dropped grants for a revocation. |
|
|
301
|
+
| `onError` | Logs to the console | Called when Shopify rejects a choice or `onSync` throws. |
|
|
302
|
+
|
|
303
|
+
The bridge returns `update(snapshot)`, `connect(source)` and `setup()`.
|
|
304
|
+
`connect` follows anything with `getSnapshot` and `subscribe`, such as a c15t
|
|
305
|
+
kernel, and returns a function that stops following it.
|
|
306
|
+
|
|
307
|
+
## Limits
|
|
308
|
+
|
|
309
|
+
* Checkout runs on Shopify's domain and doesn't render your storefront code.
|
|
310
|
+
Shopify reads the stored choice there.
|
|
311
|
+
* Shopify's analytics need a real shop. mock.shop accepts the consent request
|
|
312
|
+
but sets no consent cookie, so `currentVisitorConsent()` stays empty.
|
|
313
|
+
* The bridge maps categories only; it doesn't translate IAB TCF choices.
|
|
314
|
+
|
|
315
|
+
## Verify the bridge
|
|
316
|
+
|
|
317
|
+
1. In a private window with an opt-in policy, load the page. The network panel
|
|
318
|
+
shows no `graphql.json` request with `visitorConsent`.
|
|
319
|
+
2. Click Reject All. One request goes out with every value `false`.
|
|
320
|
+
3. Open the dialog, allow Marketing and save. One request goes out with
|
|
321
|
+
`marketing` and `saleOfData` set to `true`.
|
|
322
|
+
4. On a real shop, run `window.Shopify.customerPrivacy.currentVisitorConsent()`
|
|
323
|
+
in the console and check it matches your choice.
|
|
@@ -163,7 +163,7 @@ pass them as a top-level prop:
|
|
|
163
163
|
|
|
164
164
|
```svelte title="src/App.svelte"
|
|
165
165
|
<script lang="ts">
|
|
166
|
-
import { ConsentProvider, manifest } from '
|
|
166
|
+
import { ConsentProvider, manifest } from 'c15t/svelte';
|
|
167
167
|
import { scripts } from './consent-scripts';
|
|
168
168
|
</script>
|
|
169
169
|
|
|
@@ -182,7 +182,7 @@ handle and the layout load from the [SvelteKit quickstart](https://c15t.com/docs
|
|
|
182
182
|
|
|
183
183
|
```svelte title="src/routes/+layout.svelte"
|
|
184
184
|
<script lang="ts">
|
|
185
|
-
import { ConsentRoot } from '
|
|
185
|
+
import { ConsentRoot } from 'c15t/svelte';
|
|
186
186
|
import { scripts } from '../consent-scripts';
|
|
187
187
|
|
|
188
188
|
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 '
|
|
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 '
|
|
186
|
+
import { ConsentRoot } from 'c15t/svelte';
|
|
187
187
|
import { scripts } from '../consent-scripts';
|
|
188
188
|
|
|
189
189
|
let { children, data } = $props();
|
|
@@ -165,7 +165,7 @@ pass them as a top-level prop:
|
|
|
165
165
|
|
|
166
166
|
```svelte title="src/App.svelte"
|
|
167
167
|
<script lang="ts">
|
|
168
|
-
import { ConsentProvider, manifest } from '
|
|
168
|
+
import { ConsentProvider, manifest } from 'c15t/svelte';
|
|
169
169
|
import { scripts } from './consent-scripts';
|
|
170
170
|
</script>
|
|
171
171
|
|
|
@@ -184,7 +184,7 @@ handle and the layout load from the [SvelteKit quickstart](https://c15t.com/docs
|
|
|
184
184
|
|
|
185
185
|
```svelte title="src/routes/+layout.svelte"
|
|
186
186
|
<script lang="ts">
|
|
187
|
-
import { ConsentRoot } from '
|
|
187
|
+
import { ConsentRoot } from 'c15t/svelte';
|
|
188
188
|
import { scripts } from '../consent-scripts';
|
|
189
189
|
|
|
190
190
|
let { children, data } = $props();
|
|
@@ -165,7 +165,7 @@ pass them as a top-level prop:
|
|
|
165
165
|
|
|
166
166
|
```svelte title="src/App.svelte"
|
|
167
167
|
<script lang="ts">
|
|
168
|
-
import { ConsentProvider, manifest } from '
|
|
168
|
+
import { ConsentProvider, manifest } from 'c15t/svelte';
|
|
169
169
|
import { scripts } from './consent-scripts';
|
|
170
170
|
</script>
|
|
171
171
|
|
|
@@ -184,7 +184,7 @@ handle and the layout load from the [SvelteKit quickstart](https://c15t.com/docs
|
|
|
184
184
|
|
|
185
185
|
```svelte title="src/routes/+layout.svelte"
|
|
186
186
|
<script lang="ts">
|
|
187
|
-
import { ConsentRoot } from '
|
|
187
|
+
import { ConsentRoot } from 'c15t/svelte';
|
|
188
188
|
import { scripts } from '../consent-scripts';
|
|
189
189
|
|
|
190
190
|
let { children, data } = $props();
|
|
@@ -169,7 +169,7 @@ pass them as a top-level prop:
|
|
|
169
169
|
|
|
170
170
|
```svelte title="src/App.svelte"
|
|
171
171
|
<script lang="ts">
|
|
172
|
-
import { ConsentProvider, manifest } from '
|
|
172
|
+
import { ConsentProvider, manifest } from 'c15t/svelte';
|
|
173
173
|
import { scripts } from './consent-scripts';
|
|
174
174
|
</script>
|
|
175
175
|
|
|
@@ -188,7 +188,7 @@ handle and the layout load from the [SvelteKit quickstart](https://c15t.com/docs
|
|
|
188
188
|
|
|
189
189
|
```svelte title="src/routes/+layout.svelte"
|
|
190
190
|
<script lang="ts">
|
|
191
|
-
import { ConsentRoot } from '
|
|
191
|
+
import { ConsentRoot } from 'c15t/svelte';
|
|
192
192
|
import { scripts } from '../consent-scripts';
|
|
193
193
|
|
|
194
194
|
let { children, data } = $props();
|
|
@@ -276,6 +276,11 @@ The collector holds one `beforeSend` callback. Pass yours through the
|
|
|
276
276
|
`beforeSend` option. Calling `window.si('beforeSend', ...)` yourself replaces
|
|
277
277
|
the helper's consent check.
|
|
278
278
|
|
|
279
|
+
A framework that starts allowed scripts from the server HTML starts the
|
|
280
|
+
collector before the app hydrates, and the helper registers its callback at
|
|
281
|
+
hydration. With a `beforeSend` option, the collector waits for hydration
|
|
282
|
+
instead, so your callback sees every report.
|
|
283
|
+
|
|
279
284
|
Reports only cover page loads after consent. The collector reads buffered
|
|
280
285
|
paint and navigation timings, but interactions before consent are missed. The
|
|
281
286
|
helper does not set `data-route`, so Vercel groups reports by URL rather than
|
|
@@ -163,7 +163,7 @@ pass them as a top-level prop:
|
|
|
163
163
|
|
|
164
164
|
```svelte title="src/App.svelte"
|
|
165
165
|
<script lang="ts">
|
|
166
|
-
import { ConsentProvider, manifest } from '
|
|
166
|
+
import { ConsentProvider, manifest } from 'c15t/svelte';
|
|
167
167
|
import { scripts } from './consent-scripts';
|
|
168
168
|
</script>
|
|
169
169
|
|
|
@@ -182,7 +182,7 @@ handle and the layout load from the [SvelteKit quickstart](https://c15t.com/docs
|
|
|
182
182
|
|
|
183
183
|
```svelte title="src/routes/+layout.svelte"
|
|
184
184
|
<script lang="ts">
|
|
185
|
-
import { ConsentRoot } from '
|
|
185
|
+
import { ConsentRoot } from 'c15t/svelte';
|
|
186
186
|
import { scripts } from '../consent-scripts';
|
|
187
187
|
|
|
188
188
|
let { children, data } = $props();
|
|
@@ -240,7 +240,7 @@ The provider from your quickstart supplies its consent state.
|
|
|
240
240
|
|
|
241
241
|
```svelte title="src/ConsentEmbed.svelte"
|
|
242
242
|
<script lang="ts">
|
|
243
|
-
import { ConsentGate } from '
|
|
243
|
+
import { ConsentGate } from 'c15t/svelte';
|
|
244
244
|
import { embedCategory, embedURL, embedTitle, embedAspectRatio } from './embed-config';
|
|
245
245
|
</script>
|
|
246
246
|
|
|
@@ -269,7 +269,7 @@ Keep the SvelteKit root and its server-resolved state unchanged.
|
|
|
269
269
|
|
|
270
270
|
```svelte title="src/lib/ConsentEmbed.svelte"
|
|
271
271
|
<script lang="ts">
|
|
272
|
-
import { ConsentGate } from '
|
|
272
|
+
import { ConsentGate } from 'c15t/svelte';
|
|
273
273
|
import { embedCategory, embedURL, embedTitle, embedAspectRatio } from '../embed-config';
|
|
274
274
|
</script>
|
|
275
275
|
|
package/package.json
CHANGED
|
@@ -1,41 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c15t/react",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "React cookie consent banner
|
|
3
|
+
"version": "3.2.0-canary.0",
|
|
4
|
+
"description": "React cookie consent banner and React consent dialog for c15t, plus a React cookie banner, preference center and headless hooks.",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"banner",
|
|
7
|
-
"ccpa",
|
|
8
6
|
"cmp",
|
|
9
7
|
"consent",
|
|
10
|
-
"consent-banner",
|
|
11
|
-
"consent-management",
|
|
12
|
-
"consent-management-platform",
|
|
13
8
|
"consent-manager",
|
|
14
|
-
"consent-manager-react",
|
|
15
|
-
"consent-mode-v2",
|
|
16
|
-
"cookie",
|
|
17
9
|
"cookie-banner",
|
|
18
10
|
"cookie-consent",
|
|
19
11
|
"gdpr",
|
|
20
|
-
"global-privacy-control",
|
|
21
|
-
"google-consent-mode",
|
|
22
12
|
"headless",
|
|
23
|
-
"iab",
|
|
24
13
|
"iab-tcf",
|
|
25
|
-
"lgpd",
|
|
26
|
-
"preference-center",
|
|
27
|
-
"privacy",
|
|
28
14
|
"react",
|
|
29
|
-
"react-cmp",
|
|
30
|
-
"react-consent",
|
|
31
15
|
"react-cookie-banner",
|
|
32
|
-
"react-cookie-consent"
|
|
33
|
-
"react-server-components",
|
|
34
|
-
"rsc",
|
|
35
|
-
"tcf",
|
|
36
|
-
"tracking-consent",
|
|
37
|
-
"typescript",
|
|
38
|
-
"use-client"
|
|
16
|
+
"react-cookie-consent"
|
|
39
17
|
],
|
|
40
18
|
"homepage": "https://c15t.com/docs/frameworks/react/quickstart",
|
|
41
19
|
"bugs": {
|
|
@@ -269,16 +247,16 @@
|
|
|
269
247
|
"test:watch": "bun prebuild && vitest"
|
|
270
248
|
},
|
|
271
249
|
"dependencies": {
|
|
272
|
-
"@c15t/core": "3.
|
|
273
|
-
"@c15t/dev-tools": "3.
|
|
274
|
-
"@c15t/iab": "3.
|
|
275
|
-
"@c15t/schema": "3.1.0",
|
|
276
|
-
"@c15t/translations": "3.
|
|
277
|
-
"@c15t/ui": "3.0.
|
|
250
|
+
"@c15t/core": "3.2.0-canary.0",
|
|
251
|
+
"@c15t/dev-tools": "3.2.0-canary.0",
|
|
252
|
+
"@c15t/iab": "3.2.0-canary.0",
|
|
253
|
+
"@c15t/schema": "3.1.1-canary.0",
|
|
254
|
+
"@c15t/translations": "3.2.0-canary.0",
|
|
255
|
+
"@c15t/ui": "3.0.2-canary.0",
|
|
278
256
|
"@types/google.maps": "3.65.3"
|
|
279
257
|
},
|
|
280
258
|
"devDependencies": {
|
|
281
|
-
"@c15t/backend": "3.
|
|
259
|
+
"@c15t/backend": "3.2.0-canary.0",
|
|
282
260
|
"@c15t/conformance": "0.0.1",
|
|
283
261
|
"@c15t/typescript-config": "0.0.1",
|
|
284
262
|
"@c15t/vitest-config": "1.0.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
let r=function(r,e){return 0===r.length&&1===e.length&&"dismiss"===e[0]?e:r};export{r as resolveBannerPrimaryActions};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Primary actions for the banner, with dismiss promoted on a bare notice.
|
|
3
|
-
*
|
|
4
|
-
* @remarks
|
|
5
|
-
* The resolver's default primary is `customize`, which a notice never
|
|
6
|
-
* offers, so a notice resolves with no primary action. When dismiss is the
|
|
7
|
-
* only control on the surface it is the primary one.
|
|
8
|
-
*
|
|
9
|
-
* @param primaryActions - Primary actions from the resolved presentation.
|
|
10
|
-
* @param orderedActions - Every action the surface renders, in order.
|
|
11
|
-
* @returns The primary actions the banner should style.
|
|
12
|
-
* @internal
|
|
13
|
-
*/
|
|
14
|
-
export declare const resolveBannerPrimaryActions: <Action extends string>(primaryActions: readonly Action[], orderedActions: readonly Action[]) => readonly Action[];
|