Skip to content

Commit 23d52ee

Browse files
committed
Add c15t consent management
1 parent 6c0dbf0 commit 23d52ee

8 files changed

Lines changed: 211 additions & 42 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
"prepare": "husky"
3636
},
3737
"dependencies": {
38+
"@c15t/react": "^2.1.0",
39+
"@c15t/scripts": "^2.1.0",
3840
"@fingerprintjs/fingerprintjs-pro-react": "^2.7.1",
3941
"@floating-ui/react": "^0.27.19",
4042
"@kapaai/react-sdk": "^0.9.10",

pnpm-lock.yaml

Lines changed: 72 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/ConsentManager.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import * as React from 'react'
2+
import type { ReactNode } from 'react'
3+
import {
4+
ConsentBanner,
5+
ConsentDialog,
6+
ConsentManagerProvider,
7+
type ConsentManagerOptions,
8+
} from '@c15t/react'
9+
import { gtag } from '@c15t/scripts/google-tag'
10+
11+
const C15T_BACKEND_URL = 'https://eager-kayak-phobos-tanstack-com.inth.app'
12+
const GOOGLE_ANALYTICS_ID = 'G-JMT1Z50SPS'
13+
const LEGAL_LINKS: Array<'privacyPolicy' | 'termsOfService'> = [
14+
'privacyPolicy',
15+
'termsOfService',
16+
]
17+
18+
const consentManagerOptions = {
19+
mode: 'hosted',
20+
backendURL: C15T_BACKEND_URL,
21+
consentCategories: ['necessary', 'measurement'],
22+
overrides: import.meta.env.DEV ? { country: 'DE' } : undefined,
23+
legalLinks: {
24+
privacyPolicy: {
25+
href: '/privacy',
26+
target: '_self',
27+
},
28+
termsOfService: {
29+
href: '/terms',
30+
target: '_self',
31+
},
32+
},
33+
scripts: [
34+
gtag({
35+
id: GOOGLE_ANALYTICS_ID,
36+
category: 'measurement',
37+
}),
38+
],
39+
} satisfies ConsentManagerOptions
40+
41+
export function ConsentManager({
42+
children,
43+
showControls = true,
44+
}: {
45+
children: ReactNode
46+
showControls?: boolean
47+
}) {
48+
const [hasMounted, setHasMounted] = React.useState(false)
49+
50+
React.useEffect(() => {
51+
setHasMounted(true)
52+
}, [])
53+
54+
// c15t injects runtime theme styles; mount after hydration so Start's SSR
55+
// markup stays byte-stable.
56+
if (!hasMounted) {
57+
return <>{children}</>
58+
}
59+
60+
return (
61+
<ConsentManagerProvider options={consentManagerOptions}>
62+
{showControls ? (
63+
<>
64+
<ConsentBanner legalLinks={LEGAL_LINKS} />
65+
<ConsentDialog legalLinks={LEGAL_LINKS} />
66+
</>
67+
) : null}
68+
{children}
69+
</ConsentManagerProvider>
70+
)
71+
}

src/components/Footer.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as React from 'react'
2+
import { ConsentDialogLink } from '@c15t/react'
13
import { Link } from '@tanstack/react-router'
24
import { Card } from './Card'
35

@@ -53,10 +55,31 @@ export function Footer() {
5355
)}
5456
</div>
5557
))}
58+
<FooterConsentSettingsLink />
5659
</div>
5760
<div className={`text-center opacity-60`}>
5861
&copy; {new Date().getFullYear()} TanStack LLC
5962
</div>
6063
</Card>
6164
)
6265
}
66+
67+
function FooterConsentSettingsLink() {
68+
const [hasMounted, setHasMounted] = React.useState(false)
69+
70+
React.useEffect(() => {
71+
setHasMounted(true)
72+
}, [])
73+
74+
if (!hasMounted) {
75+
return null
76+
}
77+
78+
return (
79+
<div>
80+
<ConsentDialogLink className="appearance-none border-0 bg-transparent p-0 text-left text-current [font:inherit]">
81+
Privacy Settings
82+
</ConsentDialogLink>
83+
</div>
84+
)
85+
}

src/routes/__root.tsx

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
3232
import { SearchProvider } from '~/contexts/SearchContext'
3333
import { ToastProvider } from '~/components/ToastProvider'
3434
import { LoginModalProvider } from '~/contexts/LoginModalContext'
35+
import { ConsentManager } from '~/components/ConsentManager'
3536

3637
import { Spinner } from '~/components/Spinner'
3738
import { ThemeProvider, useHtmlClass } from '~/components/ThemeProvider'
@@ -41,10 +42,6 @@ import { trackPageView } from '~/utils/analytics'
4142
import { createPartnerPlacementSessionSeed } from '~/utils/partner-placement'
4243
import { twMerge } from 'tailwind-merge'
4344

44-
const GOOGLE_ANALYTICS_ID = 'G-JMT1Z50SPS'
45-
const GOOGLE_ANALYTICS_PROXY_PREFIX = '/_a'
46-
const GOOGLE_ANALYTICS_SCRIPT_SRC = `${GOOGLE_ANALYTICS_PROXY_PREFIX}/gtag.js`
47-
const GOOGLE_ANALYTICS_BOOTSTRAP = `(function(){var id='${GOOGLE_ANALYTICS_ID}';var src='${GOOGLE_ANALYTICS_SCRIPT_SRC}';window.dataLayer=window.dataLayer||[];window.gtag=window.gtag||function(){window.dataLayer.push(arguments)};window.gtag('js',new Date());window.gtag('config',id,{transport_url:window.location.origin+'${GOOGLE_ANALYTICS_PROXY_PREFIX}'});var loaded=false;var load=function(){if(loaded)return;loaded=true;var script=document.createElement('script');script.async=true;script.src=src;script.setAttribute('data-ga-loader','true');document.head.appendChild(script)};if(typeof window.requestIdleCallback==='function'){window.requestIdleCallback(load,{timeout:3000});return}if(document.readyState==='complete'){window.setTimeout(load,1500);return}window.addEventListener('load',function(){window.setTimeout(load,1500)},{once:true})})();`
4845
const DOCUMENT_CACHE_HEADERS = {
4946
'Cache-Control': 'public, max-age=0, must-revalidate',
5047
'Cloudflare-CDN-Cache-Control': 'no-store',
@@ -223,9 +220,6 @@ export const Route = createRootRouteWithContext<{
223220
{
224221
children: `(function(){try{var t=localStorage.getItem('theme')||'auto';var v=['light','dark','auto'].includes(t)?t:'auto';if(v==='auto'){var a=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';document.documentElement.classList.add(a,'auto')}else{document.documentElement.classList.add(v)}}catch(e){var a=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';document.documentElement.classList.add(a,'auto')}})()`,
225222
},
226-
{
227-
children: GOOGLE_ANALYTICS_BOOTSTRAP,
228-
},
229223
],
230224
}
231225
},
@@ -305,44 +299,46 @@ function ShellComponent({ children }: { children: React.ReactNode }) {
305299
{hasBaseParent ? <base target="_parent" /> : null}
306300
</head>
307301
<body className="overflow-x-hidden">
308-
<LoginModalProvider>
309-
<ToastProvider>
310-
<PageViewTracker />
311-
{hideNavbar ? children : <Navbar>{children}</Navbar>}
312-
{showDevtools && LazyAppDevtools ? (
313-
<OptionalDevtoolsBoundary>
314-
<React.Suspense fallback={null}>
315-
<LazyAppDevtools />
316-
</React.Suspense>
317-
</OptionalDevtoolsBoundary>
318-
) : null}
319-
<div
320-
aria-hidden="true"
321-
className={twMerge(
322-
'pointer-events-none fixed top-0 left-0 z-99999999 h-[320px] w-full select-none',
323-
)}
324-
>
325-
<div
326-
className={twMerge(
327-
'absolute top-0 w-full h-80 rounded-[100%] bg-amber-500/30 blur-3xl transition-all duration-500 dark:bg-sky-400/25',
328-
showNavigationSpinner
329-
? '-translate-y-1/2 opacity-100'
330-
: '-translate-y-full opacity-0',
331-
)}
332-
/>
302+
<ConsentManager showControls={!hasBaseParent}>
303+
<LoginModalProvider>
304+
<ToastProvider>
305+
<PageViewTracker />
306+
{hideNavbar ? children : <Navbar>{children}</Navbar>}
307+
{showDevtools && LazyAppDevtools ? (
308+
<OptionalDevtoolsBoundary>
309+
<React.Suspense fallback={null}>
310+
<LazyAppDevtools />
311+
</React.Suspense>
312+
</OptionalDevtoolsBoundary>
313+
) : null}
333314
<div
315+
aria-hidden="true"
334316
className={twMerge(
335-
'absolute top-6 left-1/2 -translate-x-1/2 rounded-full bg-white/75 p-2 shadow-lg backdrop-blur-lg transition-all duration-300 dark:bg-slate-900/40',
336-
showNavigationSpinner
337-
? 'translate-y-0 opacity-100'
338-
: '-translate-y-6 opacity-0',
317+
'pointer-events-none fixed top-0 left-0 z-99999999 h-[320px] w-full select-none',
339318
)}
340319
>
341-
<Spinner className="text-4xl" />
320+
<div
321+
className={twMerge(
322+
'absolute top-0 w-full h-80 rounded-[100%] bg-amber-500/30 blur-3xl transition-all duration-500 dark:bg-sky-400/25',
323+
showNavigationSpinner
324+
? '-translate-y-1/2 opacity-100'
325+
: '-translate-y-full opacity-0',
326+
)}
327+
/>
328+
<div
329+
className={twMerge(
330+
'absolute top-6 left-1/2 -translate-x-1/2 rounded-full bg-white/75 p-2 shadow-lg backdrop-blur-lg transition-all duration-300 dark:bg-slate-900/40',
331+
showNavigationSpinner
332+
? 'translate-y-0 opacity-100'
333+
: '-translate-y-6 opacity-0',
334+
)}
335+
>
336+
<Spinner className="text-4xl" />
337+
</div>
342338
</div>
343-
</div>
344-
</ToastProvider>
345-
</LoginModalProvider>
339+
</ToastProvider>
340+
</LoginModalProvider>
341+
</ConsentManager>
346342
<Scripts />
347343
</body>
348344
</html>

src/styles/app.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@import 'tailwindcss';
2+
@import '@c15t/react/styles.css';
23

34
@plugin "@tailwindcss/typography";
45

src/utils/analytics/providers/google.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import type { AnalyticsProvider } from '../types'
22

33
declare global {
44
interface Window {
5-
dataLayer: unknown[] | undefined
6-
gtag: ((...args: unknown[]) => void) | undefined
5+
dataLayer: unknown[]
6+
gtag: (...args: unknown[]) => void
77
}
88
}
99

vite.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ const useSyncExternalStoreShimIndexAlias = {
7575
// Worker server output so the runtime never loads their raw package entries.
7676
const serverBundledClientPackages = [
7777
...(shouldUseRedact ? ['@tanstack/redact'] : []),
78+
'@c15t/react',
79+
'@c15t/scripts',
80+
'@c15t/ui',
81+
'c15t',
7882
/^@radix-ui\//,
7983
'@kapaai/react-sdk',
8084
'@tanstack/highlight',

0 commit comments

Comments
 (0)