Add support for custom cookie banner registration (#3925)

This commit is contained in:
Nolann B.
2026-02-10 11:28:24 +01:00
committed by GitHub
parent 10754fbaf4
commit 7a11861d36
8 changed files with 159 additions and 14 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/browser-types": patch
"gitbook": patch
---
Add support for custom cookie banner registration
+17
View File
@@ -46,6 +46,11 @@ export type GitBookAssistant = {
ui?: boolean;
};
export type GitBookCookieBannerHandler = (options: {
onApprove: () => void;
onReject: () => void;
}) => void;
export type GitBookGlobal = {
/**
* Register an event listener.
@@ -72,6 +77,18 @@ export type GitBookGlobal = {
* Register a custom assistant to be available on the site.
*/
registerAssistant: (assistant: GitBookAssistant) => () => void;
/**
* Register a custom cookie banner handler.
* When registered, the built-in cookie banner will not be displayed.
*/
registerCookieBanner: (handler: GitBookCookieBannerHandler) => void;
/**
* Whether the user has granted cookie consent.
* Returns true if cookies are granted, false if rejected or unknown.
*/
hasApprovedCookies: () => boolean;
};
declare global {
@@ -0,0 +1,52 @@
import { expect } from '@playwright/test';
import { type TestsCase, getCustomizationURL, runTestCases } from './util';
const testCases: TestsCase[] = [
{
name: 'Cookie Banner',
contentBaseURL: 'https://gitbook.com/docs/',
tests: [
{
name: 'should show built-in banner when no custom banner is registered',
url: getCustomizationURL({
privacyPolicy: {
url: 'https://policies.gitbook.com/privacy/cookies',
},
}),
screenshot: false,
run: async (page) => {
// Check that built-in banner is visible
const dialog = page.getByTestId('cookies-dialog');
await expect(dialog).toBeVisible({ timeout: 5000 });
},
},
{
name: 'should not show built-in banner when custom banner is registered',
url: getCustomizationURL({
privacyPolicy: {
url: 'https://policies.gitbook.com/privacy/cookies',
},
}),
screenshot: false,
run: async (page) => {
// Register a custom cookie banner handler
await page.waitForFunction(() => {
return typeof window !== 'undefined' && window.GitBook !== undefined;
});
await page.evaluate(() => {
window.GitBook?.registerCookieBanner(() => {
// Custom cookie banner handler - no-op for testing to avoid reload
});
});
// Check that built-in banner is not visible
const dialog = page.getByTestId('cookies-dialog');
await expect(dialog).not.toBeVisible({ timeout: 5000 });
},
},
],
},
];
runTestCases(testCases);
+1 -1
View File
@@ -120,7 +120,7 @@
"dev:cloudflare": "wrangler dev --port 8771 --env preview",
"dev:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
"dev:cf:server": "wrangler dev --port 8772 --env dev --config ./openNext/customWorkers/defaultWrangler.jsonc",
"e2e": "playwright test e2e/internal.spec.ts e2e/pdf.spec.ts --project=chromium",
"e2e": "playwright test e2e/internal.spec.ts e2e/cookie-banner.spec.ts e2e/pdf.spec.ts --project=chromium",
"e2e-customers": "playwright test e2e/customers.spec.ts --project=chromium",
"unit": "bun test {src,packages} --preload ./tests/preload-bun.ts",
"e2e-browserless": "bun test ./tests/",
@@ -6,6 +6,7 @@ import { useLanguage } from '@/intl/client';
import { t, tString } from '@/intl/translate';
import { tcls } from '@/lib/tailwind';
import { useCustomCookieBanner, useIntegrationsLoaded } from '@/components/Integrations';
import { isCookiesTrackingDisabled, setCookiesTracking } from '../Insights';
/**
@@ -15,10 +16,18 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
const { privacyPolicy = 'https://policies.gitbook.com/privacy/cookies' } = props;
const [show, setShow] = React.useState(false);
const language = useLanguage();
const integrationsLoaded = useIntegrationsLoaded();
const { hasCustomCookieBanner } = useCustomCookieBanner();
React.useEffect(() => {
// Always wait for integrations to load, and if a custom banner is registered, hide the built-in banner
if (!integrationsLoaded || hasCustomCookieBanner) {
setShow(false);
return;
}
setShow(isCookiesTrackingDisabled() === undefined);
}, []);
}, [hasCustomCookieBanner, integrationsLoaded]);
if (!show) {
return null;
@@ -31,3 +31,12 @@ export function isCookiesTrackingDisabled() {
return undefined;
}
/**
* Return true if cookies are accepted.
* Return false if cookies are rejected or unknown.
*/
export function hasApprovedCookies() {
const state = getBrowserCookie(GRANTED_COOKIE);
return state === 'yes';
}
@@ -1,14 +1,14 @@
'use client';
import * as React from 'react';
import * as zustand from 'zustand';
import { hasApprovedCookies, setCookiesTracking } from '@/components/Insights';
import type {
GitBookGlobal,
GitBookIntegrationEvent,
GitBookIntegrationEventCallback,
GitBookIntegrationTool,
} from '@gitbook/browser-types';
import * as React from 'react';
import * as zustand from 'zustand';
import type { Assistant } from '../AI';
const events = new Map<GitBookIntegrationEvent, GitBookIntegrationEventCallback[]>();
@@ -26,6 +26,26 @@ export const integrationsAssistantTools = zustand.createStore<{
export const integrationAssistants = zustand.createStore<Array<Assistant>>(() => []);
// Store to track when integrations have been loaded
export const integrationsStore = zustand.createStore<{
loaded: boolean;
}>(() => {
return {
loaded: false,
};
});
type CustomCookieBannerStore = {
hasCustomCookieBanner: boolean;
};
// Store for custom cookie banner registration
export const customCookieBannerStore = zustand.createStore<CustomCookieBannerStore>(() => {
return {
hasCustomCookieBanner: false,
};
});
if (typeof window !== 'undefined') {
const gitbookGlobal: GitBookGlobal = {
addEventListener: (event, callback) => {
@@ -65,6 +85,24 @@ if (typeof window !== 'undefined') {
integrationAssistants.setState((state) => state.filter((a) => a.id !== id), true);
};
},
registerCookieBanner: (handler) => {
customCookieBannerStore.setState((state) => ({
...state,
hasCustomCookieBanner: true,
}));
handler({
onApprove: () => {
setCookiesTracking(true);
window.location.reload();
},
onReject: () => {
setCookiesTracking(false);
window.location.reload();
},
});
},
hasApprovedCookies: hasApprovedCookies,
};
window.GitBook = gitbookGlobal;
}
@@ -76,12 +114,30 @@ export function useIntegrationAssistants(): Array<Assistant> {
return zustand.useStore(integrationAssistants);
}
/**
* Hook to check if integrations have been loaded.
*/
export function useIntegrationsLoaded(): boolean {
return zustand.useStore(integrationsStore, (state) => state.loaded);
}
/**
* Hook to check if a custom cookie banner is registered.
*/
export function useCustomCookieBanner(): CustomCookieBannerStore {
return zustand.useStore(customCookieBannerStore);
}
/**
* Dispatch the `load` event to all integrations.
*/
export function LoadIntegrations() {
React.useEffect(() => {
// Only dispatch 'load' event when there are scripts to load
dispatchGitBookIntegrationEvent('load');
integrationsStore.setState({ loaded: true });
}, []);
return null;
}
@@ -89,6 +145,6 @@ export function LoadIntegrations() {
/**
* Client function to dispatch a GitBook event.
*/
function dispatchGitBookIntegrationEvent(type: GitBookIntegrationEvent, ...args: any[]) {
function dispatchGitBookIntegrationEvent(type: GitBookIntegrationEvent, ...args: unknown[]) {
events.get(type)?.forEach((handler) => handler(...args));
}
@@ -68,14 +68,10 @@ export async function SiteLayout(props: {
</SpaceLayout>
</AIContextProvider>
{scripts.length > 0 ? (
<>
<LoadIntegrations />
{scripts.map(({ script }) => (
<script key={script} async src={script} />
))}
</>
) : null}
<LoadIntegrations />
{scripts.length > 0
? scripts.map(({ script }) => <script key={script} async src={script} />)
: null}
{scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
<React.Suspense fallback={null}>