Show cookies approval toast (#55)

* Show cookies toast when privacy policy is enabled

* Flex buttons

* Improve design

* Fix mobile design
This commit is contained in:
Samy Pessé
2023-12-20 16:51:14 +01:00
committed by GitHub
parent ca3ab044c3
commit fbc2ae10a0
5 changed files with 162 additions and 2 deletions
@@ -1,7 +1,9 @@
import { CustomizationThemeMode } from '@gitbook/api';
import { Metadata, Viewport } from 'next';
import { notFound, redirect } from 'next/navigation';
import React from 'react';
import { CookiesToast } from '@/components/Cookies';
import { SpaceContent } from '@/components/SpaceContent';
import { getSpaceLanguage } from '@/intl/server';
import { PageHrefContext, absoluteHref, baseUrl, pageHref } from '@/lib/links';
@@ -29,6 +31,7 @@ export default async function Page(props: { params: PagePathParams }) {
collectionSpaces,
ancestors,
document,
scripts,
} = await fetchPageData(params);
const linksContext: PageHrefContext = {};
@@ -53,6 +56,11 @@ export default async function Page(props: { params: PagePathParams }) {
collection={collection}
collectionSpaces={collectionSpaces}
/>
{scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
<React.Suspense fallback={null}>
<CookiesToast privacyPolicy={customization.privacyPolicy.url} />
</React.Suspense>
) : null}
</ClientContexts>
);
}
+110
View File
@@ -0,0 +1,110 @@
'use client';
import * as React from 'react';
import { useLanguage } from '@/intl/client';
import { t } from '@/intl/translate';
import { isCookiesTrackingDisabled, setCookiesTracking } from '@/lib/analytics';
import { tcls } from '@/lib/tailwind';
/**
* Toast to accept or reject the use of cookies.
*/
export function CookiesToast(props: { privacyPolicy?: string }) {
const { privacyPolicy = 'https://policies.gitbook.com/privacy/cookies' } = props;
const [show, setShow] = React.useState(false);
const language = useLanguage();
React.useEffect(() => {
setShow(isCookiesTrackingDisabled() === undefined);
}, []);
if (!show) {
return null;
}
const onUpdateState = (enabled: boolean) => {
setCookiesTracking(enabled);
// Reload the page to take the change in consideration
window.location.reload();
};
return (
<div
className={tcls(
'fixed',
'z-10',
'bg-white',
'dark:bg-slate-800',
'rounded',
'border-slate-400',
'dark:border-slate-700',
'shadow-md',
'p-4',
'bottom-4',
'right-4',
'left-4',
'max-w-sm',
'sm:left-auto',
)}
>
<p className={tcls('text-sm')}>
{t(
language,
'cookies_prompt',
<a
href={privacyPolicy}
className={tcls('text-primary-500', 'hover:text-primary-700', 'underline')}
>
{t(language, 'cookies_prompt_privacy')}
</a>,
)}
</p>
<div className={tcls('mt-3', 'flex', 'flex-row', 'gap-4')}>
<ToastButton
onClick={() => {
onUpdateState(true);
}}
>
{t(language, 'cookies_accept')}
</ToastButton>
<ToastButton
onClick={() => {
onUpdateState(false);
}}
>
{t(language, 'cookies_reject')}
</ToastButton>
</div>
</div>
);
}
function ToastButton(props: { onClick: () => void; children: React.ReactNode }) {
const { onClick, children } = props;
return (
<button
onClick={onClick}
className={tcls(
'bg-white',
'dark:bg-slate-800',
'text-xs',
'text-slate-800',
'dark:text-slate-200',
'rounded',
'border',
'border-slate-300',
'dark:border-slate-700',
'px-2',
'py-1',
'shadow-md',
'hover:bg-slate-100',
'dark:hover:bg-slate-700',
)}
>
{children}
</button>
);
}
+1
View File
@@ -0,0 +1 @@
export * from './CookiesToast';
+5 -1
View File
@@ -18,5 +18,9 @@
"annotation_button_label": "Open annotation",
"code_copied": "Copied!",
"code_copy": "Copy",
"table_of_contents_button_label": "Open table of contents"
"table_of_contents_button_label": "Open table of contents",
"cookies_prompt": "This site uses cookies to deliver its service and to analyse traffic. By browsing this site, you accept the ${1}.",
"cookies_prompt_privacy": "privacy policy",
"cookies_accept": "Accept",
"cookies_reject": "Reject"
}
+38 -1
View File
@@ -3,6 +3,7 @@
import cookies from 'js-cookie';
const VISITORID_COOKIE = '__session';
const GRANTED_COOKIE = '__gitbook_cookie_granted';
let visitorId: string | null = null;
let pendingVisitorId: Promise<string> | null = null;
@@ -28,6 +29,12 @@ export async function getVisitorId(): Promise<string> {
* Propose a visitor identifier to the GitBook.com server and get the devideId back.
*/
async function fetchVisitorID(): Promise<string> {
const withoutCookies = isCookiesTrackingDisabled();
if (withoutCookies) {
return getNewVisitorId();
}
const existingTrackingCookie = cookies.get(VISITORID_COOKIE);
if (existingTrackingCookie) {
@@ -35,7 +42,7 @@ async function fetchVisitorID(): Promise<string> {
return existingTrackingCookie;
} else {
// No tracking deviceId set, we'll need to consolidate with the server.
const proposed = `${crypto.randomUUID()}R`;
const proposed = getNewVisitorId();
const url = new URL(process.env.NEXT_PUBLIC_GITBOOK_APP_URL ?? `https://app.gitbook.com`);
url.pathname = '/__session';
@@ -56,3 +63,33 @@ async function fetchVisitorID(): Promise<string> {
}
}
}
/**
* Accept or reject cookies.
*/
export function setCookiesTracking(enabled: boolean) {
cookies.set(GRANTED_COOKIE, enabled ? 'yes' : 'no');
}
/**
* Return true if cookies are accepted or not.
* Return `undefined` if state is not known.
*/
export function isCookiesTrackingDisabled() {
const state = cookies.get(GRANTED_COOKIE);
if (state === 'yes') {
return false;
} else if (state === 'no') {
return true;
}
return undefined;
}
/**
* Get a proposed visitor ID.
*/
function getNewVisitorId(): string {
return `${crypto.randomUUID()}R`;
}