mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-25 11:52:10 +00:00
Add support for custom cookie banner registration (#3925)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@gitbook/browser-types": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add support for custom cookie banner registration
|
||||
@@ -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);
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user