Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/components/Footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down
22 changes: 6 additions & 16 deletions src/components/GoogleTagManager.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,18 @@ 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);
})(window,document,'script','dataLayer','${GTM_ID}');`}
</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>
);
};
40 changes: 37 additions & 3 deletions src/components/Matomo.jsx
Original file line number Diff line number Diff line change
@@ -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>
);
Expand Down
75 changes: 63 additions & 12 deletions src/components/cookie-consent/CookieConsentProvider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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}
Expand Down
2 changes: 2 additions & 0 deletions src/pages/_app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -33,6 +34,7 @@ function AppInner({ Component, pageProps }) {
return (
<>
<MatomoTagManager consentGiven={isAccepted} />
<GoogleTagManager consentGiven={isAccepted} />
<Head>
<style>{dom.css()}</style>
{router.route.startsWith('/ipa') ?
Expand Down
3 changes: 0 additions & 3 deletions src/pages/_document.jsx
Original file line number Diff line number Diff line change
@@ -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)')
Expand Down Expand Up @@ -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>
Expand Down
Loading