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

View File

@@ -6,6 +6,7 @@ import { Transition } from '@headlessui/react'
import { Button } from '@/components/Button'
import {apiNavigation, flattenNavItems} from '@/components/NavigationAPI'
import {docsNavigation} from "@/components/NavigationDocs";
import {useCookieConsent} from '@/components/cookie-consent/CookieConsentProvider'
function CheckIcon(props) {
return (
@@ -227,11 +228,20 @@ function SocialLink({ href, icon: Icon, children }) {
}
function SmallPrint() {
const { openCookieSettings } = useCookieConsent()
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">
<p className="text-xs text-zinc-600 dark:text-zinc-400">
&copy; Copyright {new Date().getFullYear()}. All rights reserved.
</p>
<div className="flex items-center gap-4 text-xs text-zinc-600 dark:text-zinc-400">
<p>&copy; Copyright {new Date().getFullYear()}. All rights reserved.</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">
<SocialLink href="https://x.com/netbird" icon={TwitterIcon}>
Follow us on X

View File

@@ -4,10 +4,14 @@ import Script from "next/script";
// Google Tag Manager ID
const GTM_ID = "GTM-PGWDPDN3";
export const GoogleTagManagerHeadScript = () => {
export const GoogleTagManager = ({ consentGiven }) => {
if (!consentGiven) {
return null;
}
return (
<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],
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);
@@ -15,17 +19,3 @@ export const GoogleTagManagerHeadScript = () => {
</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>
);
};

View File

@@ -1,16 +1,50 @@
import Script from "next/script";
import { useEffect, useRef } from 'react'
import { useRouter } from 'next/router'
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 (
<Script id="matomo-tag-manager" strategy="afterInteractive">
{`var _paq = window._paq = window._paq || [];
_paq.push(['requireCookieConsent']);
${consentGiven ? "_paq.push(['setCookieConsentGiven']);" : ""}
_paq.push(['requireConsent']);
var _mtm = window._mtm = window._mtm || [];
_mtm.push({'mtm.startTime': (new Date().getTime()), 'event': 'mtm.Start'});
(function() {
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>
);

View File

@@ -2,14 +2,28 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea
import { useRouter } from 'next/router'
const STORAGE_KEY = 'cookie-consent'
const ACCEPT_EXPIRY_DAYS = 90
const DECLINE_EXPIRY_DAYS = 1
const CONSENT_EXPIRY_DAYS = 180
const TRACKING_COOKIE_PREFIXES = [
'_ga',
'_gid',
'_hj',
'_clck',
'_clsk',
'_gcl_',
'_pk_',
'__hst',
'hubspotutk',
'messagesUtk',
]
const CookieConsentContext = createContext({
isAccepted: false,
isDeclined: false,
showConsent: false,
acceptCookies: () => {},
declineCookies: () => {},
openCookieSettings: () => {},
})
function getStoredConsent() {
@@ -40,9 +54,34 @@ function storeConsent(value, days) {
} 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 }) {
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)
useEffect(() => {
@@ -59,32 +98,44 @@ export function CookieConsentProvider({ children }) {
}, [router.pathname])
const acceptCookies = useCallback(() => {
storeConsent('accepted', ACCEPT_EXPIRY_DAYS)
storeConsent('accepted', CONSENT_EXPIRY_DAYS)
setConsent('accepted')
setShowConsent(false)
// Enable Matomo cookies
window._paq = window._paq || []
window._paq.push(['setCookieConsentGiven'])
}, [])
const declineCookies = useCallback(() => {
storeConsent('declined', DECLINE_EXPIRY_DAYS)
const trackersWereActive = consent === 'accepted'
storeConsent('declined', CONSENT_EXPIRY_DAYS)
setConsent('declined')
setShowConsent(false)
// Tell Matomo to forget consent and delete its cookies
window._paq = window._paq || []
window._paq.push(['forgetCookieConsentGiven'])
removeTrackingCookies()
// 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 (
<CookieConsentContext.Provider
value={{
isAccepted: consent === 'accepted',
isDeclined: consent === 'declined',
showConsent,
acceptCookies,
declineCookies,
openCookieSettings,
}}
>
{children}

View File

@@ -15,6 +15,7 @@ import {dom} from "@fortawesome/fontawesome-svg-core";
import {AnnouncementBannerProvider} from "@/components/announcement-banner/AnnouncementBannerProvider";
import {ImageZoom} from "@/components/ImageZoom";
import {MatomoTagManager} from "@/components/Matomo";
import {GoogleTagManager} from "@/components/GoogleTagManager";
import {CookieConsentProvider, useCookieConsent} from "@/components/cookie-consent/CookieConsentProvider";
import {CookieConsent} from "@/components/cookie-consent/CookieConsent";
@@ -33,6 +34,7 @@ function AppInner({ Component, pageProps }) {
return (
<>
<MatomoTagManager consentGiven={isAccepted} />
<GoogleTagManager consentGiven={isAccepted} />
<Head>
<style>{dom.css()}</style>
{router.route.startsWith('/ipa') ?

View File

@@ -1,5 +1,4 @@
import { Head, Html, Main, NextScript } from 'next/document'
import {GoogleTageManagerBodyScript, GoogleTagManagerHeadScript} from "@/components/GoogleTagManager";
const modeScript = `
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
@@ -40,12 +39,10 @@ export default function Document() {
return (
<Html lang="en">
<Head>
<GoogleTagManagerHeadScript />
<script dangerouslySetInnerHTML={{ __html: modeScript }} />
<link rel="shortcut icon" href="/docs-static/img/favicon.ico" />
</Head>
<body className="bg-white antialiased dark:bg-[#181A1D]">
<GoogleTageManagerBodyScript />
<Main />
<NextScript />
</body>