Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cookie banner #505

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"tabWidth": 4
}
1 change: 1 addition & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// <reference lib="DOM" />
import type { Preview } from "@storybook/html";
import "decorator-client/src/main.css";
import "decorator-client/src/views/consent-banner";
import "decorator-client/src/views/breadcrumbs";
import "decorator-client/src/views/user-menu";
import "decorator-client/src/views/dropdown-menu";
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Faro } from "@grafana/faro-web-sdk";
import { AppState } from "decorator-shared/types";
import { CustomEvents, MessageEvents } from "./events";
import { BoostClient, BoostConfig } from "./views/chatbot";
import { WebStorageController } from "./webStorage";

declare global {
interface Window {
Expand Down Expand Up @@ -44,5 +45,6 @@ declare global {
ev: CustomEvent<CustomEvents[K]>,
) => void,
): void;
webstorageController: WebStorageController;
}
}
3 changes: 3 additions & 0 deletions packages/client/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export type CustomEvents = {
clearsearch: void;
closemenus: void; // Currently fired only from other apps
historyPush: void;
consentAllWebStorage: void;
refuseOptionalWebStorage: void;
showConsentBanner: void;
scrollTo: {
top?: number;
left?: number;
Expand Down
18 changes: 12 additions & 6 deletions packages/client/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { refreshAuthData } from "./helpers/auth";
import { buildHtmlElement } from "./helpers/html-element-builder";
import { param, initParams } from "./params";
import "./main.css";
import { WebStorageController } from "./webStorage";

import.meta.glob("./styles/*.css", { eager: true });
import.meta.glob(["./views/**/*.ts", "!./views/**/*.test.ts"], { eager: true });
Expand All @@ -32,18 +33,23 @@ const injectHeadAssets = () => {
};

const init = () => {
window.webstorageController = new WebStorageController();
initParams();
injectHeadAssets();
initHistoryEvents();
initScrollToEvents();

if (param("maskHotjar")) {
document.documentElement.setAttribute("data-hj-suppress", "");
}
const { allowOptional } = window.webstorageController.checkConsent();

refreshAuthData().then((response) => {
initAnalytics(response.auth);
});
if (allowOptional) {
if (param("maskHotjar")) {
document.documentElement.setAttribute("data-hj-suppress", "");
}

refreshAuthData().then((response) => {
initAnalytics(response.auth);
});
}
};

if (document.readyState === "loading") {
Expand Down
77 changes: 77 additions & 0 deletions packages/client/src/styles/consent-banner.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
.consentBanner {
overflow: visible;
border: none;
position: fixed;
bottom: 0;
right: 0;
inset: 2rem;
margin: auto 0 0 auto;
width: calc(560px - 4rem);
border-radius: var(--a-border-radius-large);

@media (max-width: 768px) {
width: 100%;
inset: 0;
margin: auto auto 2rem auto;
}
}

.consentBannerContent {
display: grid;
grid-template-columns: 1fr auto;
gap: var(--a-spacing-2);
padding: var(--a-spacing-2);
background-color: var(--a-surface-default);
border-radius: var(--a-border-radius-large);
box-shadow: var(--a-shadow-medium);
}

.title {
font-size: var(--a-font-size-large);
font-weight: bold;
margin-bottom: var(--a-spacing-2);
}

.text {
font-size: var(--a-font-size-medium);
margin-bottom: var(--a-spacing-2);
}

.button {
margin: 0;
font: inherit;
font-size: var(--a-font-size-large);
color: inherit;
cursor: pointer;
background-color: var(--a-surface-default);
padding: 0.5625rem;
display: grid;
grid-auto-flow: column;
gap: var(--a-spacing-2);
align-items: center;
border: 1px solid var(--a-gray-600);
border-radius: var(--a-border-radius-medium);
}

.buttonContainer {
display: flex;
justify-content: flex-end;
gap: var(--a-spacing-2);
}

.buttonPrimary {
font-size: var(--a-font-size-medium);
color: var(--a-white);
background-color: var(--a-blue-500);
cursor: pointer;
}

.buttonSecondary {
font-size: var(--a-font-size-medium);
color: var(--a-gray-600);
cursor: pointer;
}

.buttonDetails {
color: pink;
}
80 changes: 80 additions & 0 deletions packages/client/src/views/consent-banner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { createEvent } from "../events";
import cls from "../styles/consent-banner.module.css";
import utils from "../styles/utils.module.css";
import { defineCustomElement } from "./custom-elements";
import { isDialogDefined } from "../helpers/dialog-util";

export class ConsentBanner extends HTMLElement {
dialog!: HTMLDialogElement;
input!: HTMLInputElement;
errorList!: HTMLElement;
buttonConsentAll!: HTMLElement | null;
buttonRefuseOptional!: HTMLElement | null;

handleResponse = (
response: "CONSENT_ALL_WEB_STORAGE" | "REFUSE_OPTIONAL_WEB_STORAGE",
) => {
if (response === "CONSENT_ALL_WEB_STORAGE") {
window.dispatchEvent(createEvent("consentAllWebStorage", {}));
} else {
window.dispatchEvent(createEvent("refuseOptionalWebStorage", {}));
}
this.closeModal();
};

showModal() {
this.dialog.showModal();
this.buttonConsentAll?.focus();
}

closeModal() {
this.dialog.close();
}

checkOrWaitForWebStorageController() {
if (window.webstorageController) {
const givenConsent = window.webstorageController?.checkConsent();
if (givenConsent === null) {
this.showModal();
}
}

setTimeout(() => {
this.checkOrWaitForWebStorageController();
}, 100);
}

async connectedCallback() {
this.dialog = this.querySelector("dialog")!;
if (!isDialogDefined(this.dialog)) {
return;
}

this.buttonConsentAll = document.querySelector(
'[data-name="consent-banner-all"]',
);
this.buttonRefuseOptional = document.querySelector(
'[data-name="consent-banner-refuse-optional"]',
);

this.buttonConsentAll?.addEventListener("click", () =>
this.handleResponse("CONSENT_ALL_WEB_STORAGE"),
);
this.buttonRefuseOptional?.addEventListener("click", () =>
this.handleResponse("REFUSE_OPTIONAL_WEB_STORAGE"),
);

this.checkOrWaitForWebStorageController();
}

disconnectedCallback() {
this.buttonConsentAll?.removeEventListener("click", () =>
this.handleResponse("CONSENT_ALL_WEB_STORAGE"),
);
this.buttonRefuseOptional?.removeEventListener("click", () =>
this.handleResponse("REFUSE_OPTIONAL_WEB_STORAGE"),
);
}
}

defineCustomElement("consent-banner", ConsentBanner);
67 changes: 67 additions & 0 deletions packages/client/src/webStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Cookies from "js-cookie";

type ConsentType =
| "CONSENT_ALL_WEB_STORAGE"
| "REFUSE_OPTIONAL_WEB_STORAGE"
| null;

export class WebStorageController {
consentVersion: string = "1.0.0";
consentKey: string = `navno-consent-${this.consentVersion}`;

constructor() {
this.initEventListeners();
this.checkConsent();
console.log("WebStorageController initialized");
}

private handleConsentAllWebStorage = () => {
Cookies.set(this.consentKey, "CONSENT_ALL_WEB_STORAGE");
};

private refuseOptionalWebStorage = () => {
Cookies.set(this.consentKey, "REFUSE_OPTIONAL_WEB_STORAGE");
};

// Initialize event listeners
private initEventListeners() {
window.addEventListener(
"consentAllWebStorage",
this.handleConsentAllWebStorage,
);
window.addEventListener(
"refuseOptionalWebStorage",
this.refuseOptionalWebStorage,
);
}

private validateConsent(consent: string | undefined): ConsentType {
if (consent === undefined) {
return null;
} else {
return [
"CONSENT_ALL_WEB_STORAGE",
"REFUSE_OPTIONAL_WEB_STORAGE",
].includes(consent)
? (consent as ConsentType)
: null;
}
}

public checkConsent() {
const givenConsent = this.validateConsent(Cookies.get(this.consentKey));
return { allowOptional: givenConsent === "CONSENT_ALL_WEB_STORAGE" };
}

// Cleanup when no longer needed
destroy() {
window.removeEventListener(
"consentAllWebStorage",
this.handleConsentAllWebStorage,
);
window.removeEventListener(
"refuseOptionalWebStorage",
this.refuseOptionalWebStorage,
);
}
}
29 changes: 29 additions & 0 deletions packages/server/src/texts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ export const nb = {
did_you_find: "Fant du det du lette etter?",
search: "Søk",
search_nav_no: "Søk på nav.no",
consent_banner_title: "Denne siden bruker informasjonskapsler (cookies)",
consent_banner_text: `
<p>
Når du besøker nav.no lagres anonymiserte data i nettleseren
din, enten via informasjonskapsler (cookies) eller andre
teknologier.
</p>
<p>
Noen av disse er nødvendige for at løsningene på nav.no skal
fungere teknisk. Annen anonymisert informasjon bruker vi for å
lære mer om hvordan nav.no brukes gjennom brukerstatistikk,
klikk, navigasjon og besøksmønster. <a href="http://www.nav.no/informasjonskapsler">Se oversikt over informasjonskapsler her</a>.
</p>
`,
consent_banner_consent_all: "Godkjenn alle",
consent_banner_refuse_optional: "Avvis valgfrie",
consent_banner_configure: "Tilpass",
clear: "Tøm",
login: "Logg inn",
logout: "Logg ut",
Expand Down Expand Up @@ -111,6 +128,18 @@ const en: Texts = {
share_screen: "Share screen with your counsellor",
to_top: "To the top",
menu: "Menu",
consent_banner_title: "This page uses cookies",
consent_banner_text: `
<p>
When you visit nav.no, anonymized data is stored in your browser, either through cookies or other technologies.
</p>
<p>
Some of these are necessary for the technical functioning of the solutions on nav.no. Other anonymized information is used to learn more about how nav.no is used through user statistics, clicks, navigation, and visit patterns. This is often referred to as web analytics. <a href="http://www.nav.no/informasjonskapsler/en">See overview of cookies here</a>.
</p>
`,
consent_banner_consent_all: "Consent to all",
consent_banner_refuse_optional: "Refuse optional",
consent_banner_configure: "Configure",
close: "Close",
did_you_find: "Did you find what you were looking for?",
search: "Search",
Expand Down
29 changes: 29 additions & 0 deletions packages/server/src/views/consent-banner.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Meta, StoryObj } from "@storybook/html";
import { ConsentBanner as ClientComponent } from "decorator-client/src/views/consent-banner";
import type { ConsentBannerProps } from "./consent-banner";
import { ConsentBanner } from "./consent-banner";

const meta: Meta<ConsentBannerProps> = {
title: "consent-banner",
tags: ["autodocs"],
render: () => {
setTimeout(() => {
const modal = document.querySelector(
"consent-banner",
) as ClientComponent;

modal.showModal();
}, 0);

return ConsentBanner({ foo: "bar" });
},
};

export default meta;
type Story = StoryObj<ConsentBannerProps>;

export const Default: Story = {
args: {
foo: "bar",
},
};
Loading
Loading