From 2e495cb28aebaf12b679545be93cfb34777c254a Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:32:56 +0100 Subject: [PATCH] Add Global Privacy Control (GPC) support (#4023) --- .changeset/warm-lizards-dance.md | 6 ++++++ packages/browser-types/src/index.ts | 6 ++++++ .../src/components/Cookies/CookiesToast.tsx | 15 +++++++++++++-- .../src/components/Insights/cookies.ts | 19 +++++++++++++++++++ .../Integrations/LoadIntegrations.tsx | 7 ++++++- packages/gitbook/types/navigator.d.ts | 7 +++++++ 6 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 .changeset/warm-lizards-dance.md create mode 100644 packages/gitbook/types/navigator.d.ts diff --git a/.changeset/warm-lizards-dance.md b/.changeset/warm-lizards-dance.md new file mode 100644 index 000000000..e63b4a67c --- /dev/null +++ b/.changeset/warm-lizards-dance.md @@ -0,0 +1,6 @@ +--- +"gitbook": patch +"@gitbook/browser-types": patch +--- + +Add Global Privacy Control (GPC) support diff --git a/packages/browser-types/src/index.ts b/packages/browser-types/src/index.ts index 0dfc14dd7..93a213a25 100644 --- a/packages/browser-types/src/index.ts +++ b/packages/browser-types/src/index.ts @@ -90,6 +90,12 @@ export type GitBookGlobal = { * and `undefined` when the tracking preference is unknown or not yet determined. */ isCookiesTrackingDisabled: () => boolean | undefined; + + /** + * Indicates whether global privacy control is enabled for the current user. + * Returns `true` when global privacy control is enabled, `false` when it is disabled. + */ + isGlobalPrivacyControlEnabled: () => boolean; }; declare global { diff --git a/packages/gitbook/src/components/Cookies/CookiesToast.tsx b/packages/gitbook/src/components/Cookies/CookiesToast.tsx index babc2a33c..d32c1ad55 100644 --- a/packages/gitbook/src/components/Cookies/CookiesToast.tsx +++ b/packages/gitbook/src/components/Cookies/CookiesToast.tsx @@ -8,7 +8,11 @@ import { tcls } from '@/lib/tailwind'; import { useCustomCookieBanner, useIntegrationsLoaded } from '@/components/Integrations'; import { isAIUserAgent } from '@/lib/browser'; -import { isCookiesTrackingDisabled, setCookiesTracking } from '../Insights'; +import { + isCookiesTrackingDisabled, + isGlobalPrivacyControlEnabled, + setCookiesTracking, +} from '../Insights'; /** * Toast to accept or reject the use of cookies. @@ -20,8 +24,15 @@ export function CookiesToast(props: { privacyPolicy?: string }) { const integrationsLoaded = useIntegrationsLoaded(); const { hasCustomCookieBanner } = useCustomCookieBanner(); const isAI = isAIUserAgent(); + const hasGlobalPrivacyControl = isGlobalPrivacyControlEnabled(); React.useEffect(() => { + // If global privacy control is enabled, reject cookies + if (hasGlobalPrivacyControl && !isCookiesTrackingDisabled()) { + setCookiesTracking(false); + return; + } + // Always wait for integrations to load, and if a custom banner is registered, hide the built-in banner if (!integrationsLoaded || hasCustomCookieBanner || isAI) { setShow(false); @@ -29,7 +40,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) { } setShow(isCookiesTrackingDisabled() === undefined); - }, [hasCustomCookieBanner, integrationsLoaded, isAI]); + }, [hasCustomCookieBanner, integrationsLoaded, isAI, hasGlobalPrivacyControl]); if (!show) { return null; diff --git a/packages/gitbook/src/components/Insights/cookies.ts b/packages/gitbook/src/components/Insights/cookies.ts index 658ed4f83..d548c1c8f 100644 --- a/packages/gitbook/src/components/Insights/cookies.ts +++ b/packages/gitbook/src/components/Insights/cookies.ts @@ -31,3 +31,22 @@ export function isCookiesTrackingDisabled() { return undefined; } + +let cachedIsGlobalPrivacyControlEnabled: boolean | undefined; +/** + * Get the global privacy control settings of the user. + * Return `true` if global privacy control is enabled + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/globalPrivacyControl + */ +export function isGlobalPrivacyControlEnabled(): boolean { + if (cachedIsGlobalPrivacyControlEnabled !== undefined) { + return cachedIsGlobalPrivacyControlEnabled; + } + if (typeof navigator === 'undefined' || !('globalPrivacyControl' in navigator)) { + cachedIsGlobalPrivacyControlEnabled = false; + return false; + } + cachedIsGlobalPrivacyControlEnabled = Boolean(navigator.globalPrivacyControl); + return cachedIsGlobalPrivacyControlEnabled; +} diff --git a/packages/gitbook/src/components/Integrations/LoadIntegrations.tsx b/packages/gitbook/src/components/Integrations/LoadIntegrations.tsx index 363e754be..4910c1498 100644 --- a/packages/gitbook/src/components/Integrations/LoadIntegrations.tsx +++ b/packages/gitbook/src/components/Integrations/LoadIntegrations.tsx @@ -1,6 +1,10 @@ 'use client'; -import { isCookiesTrackingDisabled, setCookiesTracking } from '@/components/Insights'; +import { + isCookiesTrackingDisabled, + isGlobalPrivacyControlEnabled, + setCookiesTracking, +} from '@/components/Insights'; import { isAIUserAgent } from '@/lib/browser'; import type { GitBookGlobal, @@ -109,6 +113,7 @@ if (typeof window !== 'undefined') { }); }, isCookiesTrackingDisabled: isCookiesTrackingDisabled, + isGlobalPrivacyControlEnabled: isGlobalPrivacyControlEnabled, }; window.GitBook = gitbookGlobal; } diff --git a/packages/gitbook/types/navigator.d.ts b/packages/gitbook/types/navigator.d.ts new file mode 100644 index 000000000..aa4e7401f --- /dev/null +++ b/packages/gitbook/types/navigator.d.ts @@ -0,0 +1,7 @@ +declare global { + interface Navigator { + globalPrivacyControl: boolean | undefined; + } +} + +export {};