Change cookie popup behavior (#849)

This commit is contained in:
Brandon Hopkins
2026-07-24 08:28:04 -07:00
committed by GitHub
parent 8eaa109a1d
commit 24b4157011
6 changed files with 121 additions and 37 deletions
+13 -3
View File
@@ -6,6 +6,7 @@ import { Transition } from '@headlessui/react'
import { Button } from '@/components/Button' import { Button } from '@/components/Button'
import {apiNavigation, flattenNavItems} from '@/components/NavigationAPI' import {apiNavigation, flattenNavItems} from '@/components/NavigationAPI'
import {docsNavigation} from "@/components/NavigationDocs"; import {docsNavigation} from "@/components/NavigationDocs";
import {useCookieConsent} from '@/components/cookie-consent/CookieConsentProvider'
function CheckIcon(props) { function CheckIcon(props) {
return ( return (
@@ -227,11 +228,20 @@ function SocialLink({ href, icon: Icon, children }) {
} }
function SmallPrint() { function SmallPrint() {
const { openCookieSettings } = useCookieConsent()
return ( return (
<div className="flex flex-col items-center justify-between gap-5 border-t border-zinc-900/5 pt-8 dark:border-white/5 sm:flex-row"> <div className="flex flex-col items-center justify-between gap-5 border-t border-zinc-900/5 pt-8 dark:border-white/5 sm:flex-row">
<p className="text-xs text-zinc-600 dark:text-zinc-400"> <div className="flex items-center gap-4 text-xs text-zinc-600 dark:text-zinc-400">
&copy; Copyright {new Date().getFullYear()}. All rights reserved. <p>&copy; Copyright {new Date().getFullYear()}. All rights reserved.</p>
</p> <button
type="button"
onClick={openCookieSettings}
className="underline underline-offset-4 transition hover:text-zinc-900 dark:hover:text-white"
>
Cookie Settings
</button>
</div>
<div className="flex gap-4"> <div className="flex gap-4">
<SocialLink href="https://x.com/netbird" icon={TwitterIcon}> <SocialLink href="https://x.com/netbird" icon={TwitterIcon}>
Follow us on X Follow us on X
+6 -16
View File
@@ -4,10 +4,14 @@ import Script from "next/script";
// Google Tag Manager ID // Google Tag Manager ID
const GTM_ID = "GTM-PGWDPDN3"; const GTM_ID = "GTM-PGWDPDN3";
export const GoogleTagManagerHeadScript = () => { export const GoogleTagManager = ({ consentGiven }) => {
if (!consentGiven) {
return null;
}
return ( return (
<Script id="gtm-script" strategy="afterInteractive"> <Script id="gtm-script" strategy="afterInteractive">
{`(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': {`(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
@@ -15,17 +19,3 @@ export const GoogleTagManagerHeadScript = () => {
</Script> </Script>
); );
}; };
export const GoogleTageManagerBodyScript = () => {
return (
<noscript>
<iframe
title={"Google Tag Manager"}
src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
height="0"
width="0"
style={{ display: "none", visibility: "hidden" }}
/>
</noscript>
);
};
+37 -3
View File
@@ -1,16 +1,50 @@
import Script from "next/script"; import Script from "next/script";
import { useEffect, useRef } from 'react'
import { useRouter } from 'next/router'
export function MatomoTagManager({ consentGiven }) { export function MatomoTagManager({ consentGiven }) {
const router = useRouter()
const isFirstNavigation = useRef(true)
useEffect(() => {
if (!consentGiven) return
// The container tracks the initial hard load. Subsequent Next.js routes
// are emitted after the new document title has committed so Matomo does
// not associate the previous page's title with the new URL.
if (isFirstNavigation.current) {
isFirstNavigation.current = false
return
}
const id = window.setTimeout(() => {
window._mtm = window._mtm || []
window._mtm.push({
event: 'mtm.SpaPageView',
'mtm.pageUrl': window.location.href,
'mtm.pageTitle': document.title,
})
}, 0)
return () => window.clearTimeout(id)
}, [consentGiven, router.asPath])
if (!consentGiven) {
return null
}
return ( return (
<Script id="matomo-tag-manager" strategy="afterInteractive"> <Script id="matomo-tag-manager" strategy="afterInteractive">
{`var _paq = window._paq = window._paq || []; {`var _paq = window._paq = window._paq || [];
_paq.push(['requireCookieConsent']); _paq.push(['requireConsent']);
${consentGiven ? "_paq.push(['setCookieConsentGiven']);" : ""}
var _mtm = window._mtm = window._mtm || []; var _mtm = window._mtm = window._mtm || [];
_mtm.push({'mtm.startTime': (new Date().getTime()), 'event': 'mtm.Start'}); _mtm.push({'mtm.startTime': (new Date().getTime()), 'event': 'mtm.Start'});
(function() { (function() {
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src='https://cdn.matomo.cloud/netbird.matomo.cloud/container_hvVzPZGH.js'; s.parentNode.insertBefore(g,s); g.async=true;
g.src='https://cdn.matomo.cloud/netbird.matomo.cloud/container_hvVzPZGH.js';
g.onload=function(){window._paq.push(['setConsentGiven']);};
s.parentNode.insertBefore(g,s);
})();`} })();`}
</Script> </Script>
); );
@@ -2,14 +2,28 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
const STORAGE_KEY = 'cookie-consent' const STORAGE_KEY = 'cookie-consent'
const ACCEPT_EXPIRY_DAYS = 90 const CONSENT_EXPIRY_DAYS = 180
const DECLINE_EXPIRY_DAYS = 1
const TRACKING_COOKIE_PREFIXES = [
'_ga',
'_gid',
'_hj',
'_clck',
'_clsk',
'_gcl_',
'_pk_',
'__hst',
'hubspotutk',
'messagesUtk',
]
const CookieConsentContext = createContext({ const CookieConsentContext = createContext({
isAccepted: false, isAccepted: false,
isDeclined: false,
showConsent: false, showConsent: false,
acceptCookies: () => {}, acceptCookies: () => {},
declineCookies: () => {}, declineCookies: () => {},
openCookieSettings: () => {},
}) })
function getStoredConsent() { function getStoredConsent() {
@@ -40,9 +54,34 @@ function storeConsent(value, days) {
} catch {} } catch {}
} }
function removeTrackingCookies() {
const cookieNames = document.cookie
.split(';')
.map((cookie) => cookie.split('=')[0].trim())
.filter(Boolean)
for (const name of cookieNames) {
if (!TRACKING_COOKIE_PREFIXES.some((prefix) => name.startsWith(prefix))) {
continue
}
const expiredCookie = `${name}=; Max-Age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Lax`
document.cookie = expiredCookie
document.cookie = `${expiredCookie}; domain=${window.location.hostname}`
if (
window.location.hostname === 'netbird.io' ||
window.location.hostname.endsWith('.netbird.io')
) {
document.cookie = `${expiredCookie}; domain=.netbird.io`
}
}
}
export function CookieConsentProvider({ children }) { export function CookieConsentProvider({ children }) {
const router = useRouter() const router = useRouter()
const [consent, setConsent] = useState(() => getStoredConsent()) // Resolve browser storage after hydration so the server and the first client
// render agree. Analytics remains disabled while the choice is loading.
const [consent, setConsent] = useState(null)
const [showConsent, setShowConsent] = useState(false) const [showConsent, setShowConsent] = useState(false)
useEffect(() => { useEffect(() => {
@@ -59,32 +98,44 @@ export function CookieConsentProvider({ children }) {
}, [router.pathname]) }, [router.pathname])
const acceptCookies = useCallback(() => { const acceptCookies = useCallback(() => {
storeConsent('accepted', ACCEPT_EXPIRY_DAYS) storeConsent('accepted', CONSENT_EXPIRY_DAYS)
setConsent('accepted') setConsent('accepted')
setShowConsent(false) setShowConsent(false)
// Enable Matomo cookies
window._paq = window._paq || []
window._paq.push(['setCookieConsentGiven'])
}, []) }, [])
const declineCookies = useCallback(() => { const declineCookies = useCallback(() => {
storeConsent('declined', DECLINE_EXPIRY_DAYS) const trackersWereActive = consent === 'accepted'
storeConsent('declined', CONSENT_EXPIRY_DAYS)
setConsent('declined') setConsent('declined')
setShowConsent(false) setShowConsent(false)
// Tell Matomo to forget consent and delete its cookies removeTrackingCookies()
window._paq = window._paq || []
window._paq.push(['forgetCookieConsentGiven']) // Scripts already loaded by an accepted visitor cannot be reliably
// unloaded. Reload into the persisted declined state so no optional
// analytics script is mounted again.
if (trackersWereActive) {
window._paq = window._paq || []
window._paq.push(['forgetConsentGiven'])
window._paq.push(['deleteCookies'])
window.location.reload()
}
}, [consent])
const openCookieSettings = useCallback(() => {
setShowConsent(true)
}, []) }, [])
return ( return (
<CookieConsentContext.Provider <CookieConsentContext.Provider
value={{ value={{
isAccepted: consent === 'accepted', isAccepted: consent === 'accepted',
isDeclined: consent === 'declined',
showConsent, showConsent,
acceptCookies, acceptCookies,
declineCookies, declineCookies,
openCookieSettings,
}} }}
> >
{children} {children}
+2
View File
@@ -15,6 +15,7 @@ import {dom} from "@fortawesome/fontawesome-svg-core";
import {AnnouncementBannerProvider} from "@/components/announcement-banner/AnnouncementBannerProvider"; import {AnnouncementBannerProvider} from "@/components/announcement-banner/AnnouncementBannerProvider";
import {ImageZoom} from "@/components/ImageZoom"; import {ImageZoom} from "@/components/ImageZoom";
import {MatomoTagManager} from "@/components/Matomo"; import {MatomoTagManager} from "@/components/Matomo";
import {GoogleTagManager} from "@/components/GoogleTagManager";
import {CookieConsentProvider, useCookieConsent} from "@/components/cookie-consent/CookieConsentProvider"; import {CookieConsentProvider, useCookieConsent} from "@/components/cookie-consent/CookieConsentProvider";
import {CookieConsent} from "@/components/cookie-consent/CookieConsent"; import {CookieConsent} from "@/components/cookie-consent/CookieConsent";
@@ -33,6 +34,7 @@ function AppInner({ Component, pageProps }) {
return ( return (
<> <>
<MatomoTagManager consentGiven={isAccepted} /> <MatomoTagManager consentGiven={isAccepted} />
<GoogleTagManager consentGiven={isAccepted} />
<Head> <Head>
<style>{dom.css()}</style> <style>{dom.css()}</style>
{router.route.startsWith('/ipa') ? {router.route.startsWith('/ipa') ?
-3
View File
@@ -1,5 +1,4 @@
import { Head, Html, Main, NextScript } from 'next/document' import { Head, Html, Main, NextScript } from 'next/document'
import {GoogleTageManagerBodyScript, GoogleTagManagerHeadScript} from "@/components/GoogleTagManager";
const modeScript = ` const modeScript = `
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)') let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
@@ -40,12 +39,10 @@ export default function Document() {
return ( return (
<Html lang="en"> <Html lang="en">
<Head> <Head>
<GoogleTagManagerHeadScript />
<script dangerouslySetInnerHTML={{ __html: modeScript }} /> <script dangerouslySetInnerHTML={{ __html: modeScript }} />
<link rel="shortcut icon" href="/docs-static/img/favicon.ico" /> <link rel="shortcut icon" href="/docs-static/img/favicon.ico" />
</Head> </Head>
<body className="bg-white antialiased dark:bg-[#181A1D]"> <body className="bg-white antialiased dark:bg-[#181A1D]">
<GoogleTageManagerBodyScript />
<Main /> <Main />
<NextScript /> <NextScript />
</body> </body>