mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-29 13:39:11 +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;
|
ui?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GitBookCookieBannerHandler = (options: {
|
||||||
|
onApprove: () => void;
|
||||||
|
onReject: () => void;
|
||||||
|
}) => void;
|
||||||
|
|
||||||
export type GitBookGlobal = {
|
export type GitBookGlobal = {
|
||||||
/**
|
/**
|
||||||
* Register an event listener.
|
* Register an event listener.
|
||||||
@@ -72,6 +77,18 @@ export type GitBookGlobal = {
|
|||||||
* Register a custom assistant to be available on the site.
|
* Register a custom assistant to be available on the site.
|
||||||
*/
|
*/
|
||||||
registerAssistant: (assistant: GitBookAssistant) => () => void;
|
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 {
|
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: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: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",
|
"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",
|
"e2e-customers": "playwright test e2e/customers.spec.ts --project=chromium",
|
||||||
"unit": "bun test {src,packages} --preload ./tests/preload-bun.ts",
|
"unit": "bun test {src,packages} --preload ./tests/preload-bun.ts",
|
||||||
"e2e-browserless": "bun test ./tests/",
|
"e2e-browserless": "bun test ./tests/",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useLanguage } from '@/intl/client';
|
|||||||
import { t, tString } from '@/intl/translate';
|
import { t, tString } from '@/intl/translate';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
|
import { useCustomCookieBanner, useIntegrationsLoaded } from '@/components/Integrations';
|
||||||
import { isCookiesTrackingDisabled, setCookiesTracking } from '../Insights';
|
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 { privacyPolicy = 'https://policies.gitbook.com/privacy/cookies' } = props;
|
||||||
const [show, setShow] = React.useState(false);
|
const [show, setShow] = React.useState(false);
|
||||||
const language = useLanguage();
|
const language = useLanguage();
|
||||||
|
const integrationsLoaded = useIntegrationsLoaded();
|
||||||
|
const { hasCustomCookieBanner } = useCustomCookieBanner();
|
||||||
|
|
||||||
React.useEffect(() => {
|
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);
|
setShow(isCookiesTrackingDisabled() === undefined);
|
||||||
}, []);
|
}, [hasCustomCookieBanner, integrationsLoaded]);
|
||||||
|
|
||||||
if (!show) {
|
if (!show) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -31,3 +31,12 @@ export function isCookiesTrackingDisabled() {
|
|||||||
|
|
||||||
return undefined;
|
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';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import { hasApprovedCookies, setCookiesTracking } from '@/components/Insights';
|
||||||
import * as zustand from 'zustand';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
GitBookGlobal,
|
GitBookGlobal,
|
||||||
GitBookIntegrationEvent,
|
GitBookIntegrationEvent,
|
||||||
GitBookIntegrationEventCallback,
|
GitBookIntegrationEventCallback,
|
||||||
GitBookIntegrationTool,
|
GitBookIntegrationTool,
|
||||||
} from '@gitbook/browser-types';
|
} from '@gitbook/browser-types';
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as zustand from 'zustand';
|
||||||
import type { Assistant } from '../AI';
|
import type { Assistant } from '../AI';
|
||||||
|
|
||||||
const events = new Map<GitBookIntegrationEvent, GitBookIntegrationEventCallback[]>();
|
const events = new Map<GitBookIntegrationEvent, GitBookIntegrationEventCallback[]>();
|
||||||
@@ -26,6 +26,26 @@ export const integrationsAssistantTools = zustand.createStore<{
|
|||||||
|
|
||||||
export const integrationAssistants = zustand.createStore<Array<Assistant>>(() => []);
|
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') {
|
if (typeof window !== 'undefined') {
|
||||||
const gitbookGlobal: GitBookGlobal = {
|
const gitbookGlobal: GitBookGlobal = {
|
||||||
addEventListener: (event, callback) => {
|
addEventListener: (event, callback) => {
|
||||||
@@ -65,6 +85,24 @@ if (typeof window !== 'undefined') {
|
|||||||
integrationAssistants.setState((state) => state.filter((a) => a.id !== id), true);
|
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;
|
window.GitBook = gitbookGlobal;
|
||||||
}
|
}
|
||||||
@@ -76,12 +114,30 @@ export function useIntegrationAssistants(): Array<Assistant> {
|
|||||||
return zustand.useStore(integrationAssistants);
|
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.
|
* Dispatch the `load` event to all integrations.
|
||||||
*/
|
*/
|
||||||
export function LoadIntegrations() {
|
export function LoadIntegrations() {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
// Only dispatch 'load' event when there are scripts to load
|
||||||
|
|
||||||
dispatchGitBookIntegrationEvent('load');
|
dispatchGitBookIntegrationEvent('load');
|
||||||
|
|
||||||
|
integrationsStore.setState({ loaded: true });
|
||||||
}, []);
|
}, []);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -89,6 +145,6 @@ export function LoadIntegrations() {
|
|||||||
/**
|
/**
|
||||||
* Client function to dispatch a GitBook event.
|
* 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));
|
events.get(type)?.forEach((handler) => handler(...args));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,14 +68,10 @@ export async function SiteLayout(props: {
|
|||||||
</SpaceLayout>
|
</SpaceLayout>
|
||||||
</AIContextProvider>
|
</AIContextProvider>
|
||||||
|
|
||||||
{scripts.length > 0 ? (
|
<LoadIntegrations />
|
||||||
<>
|
{scripts.length > 0
|
||||||
<LoadIntegrations />
|
? scripts.map(({ script }) => <script key={script} async src={script} />)
|
||||||
{scripts.map(({ script }) => (
|
: null}
|
||||||
<script key={script} async src={script} />
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
|
{scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
|
||||||
<React.Suspense fallback={null}>
|
<React.Suspense fallback={null}>
|
||||||
|
|||||||
Reference in New Issue
Block a user