diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..5f097e0
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1 @@
+"@porscheofficial/prettier-config-porschedigital"
diff --git a/.vscode/settings.json b/.vscode/settings.json
index ac4a763..0590d6e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -5,7 +5,8 @@
".jsx",
".html",
".ts",
- ".tsx"
+ ".tsx",
+ ".test.ts"
]
},
"eslint.validate": [
@@ -15,6 +16,10 @@
"typescriptreact",
"html"
],
+ "eslint.workingDirectories": [
+ "packages/cookie-consent-banner-react",
+ "packages/cookie-consent-banner",
+ ],
"editor.formatOnSave": true,
"[javascript]": {
"editor.formatOnSave": false
@@ -29,6 +34,7 @@
"editor.formatOnSave": false
},
"editor.formatOnType": true,
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
diff --git a/README.md b/README.md
index 85e12f5..e45edd8 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ For that, some kind of component might help which provides a sleek, but customiz
That's where the Cookie Consent Banner comes in.
In order to focus on the things that really matter, the Cookie Consent Banner supports us to fulfill that requirement.
-It visualizes the available categories, like `technically required` and `analysis`, stores the decision of the user and provides an event-based API for you to handle appropriately. And of course, the consent data is stored locally in the vistors browser.
+It visualizes the available categories, like `technically required` and `analysis`, stores the decision of the user and provides an event-based API for you to handle appropriately. And of course, the consent data is stored locally in the vistor's browser.
![](./assets/example.png)
@@ -33,14 +33,14 @@ It visualizes the available categories, like `technically required` and `analysi
## :arrows_counterclockwise: Consent Flow
-The consent banner has two tasks: Provide an UI Component with which visitors of your web application can choose which cookies they want to accept and to provide you an API to react on the choosen settings.
+The consent banner has two tasks: Providing a UI Component to allow visitors of your web application to choose which cookies they want to accept, and the ability to react on the choosen settings via an API.
-There are two typical scenarios. Either the external scripts, which set cookies are managed through a tag manager of your choice using the consent data, or the scripts are loaded or configured directly on code level.
+There are two typical scenarios: Either the external scripts, which set cookies are managed through a tag manager of your choice using the consent data, or the scripts are loaded or configured directly on code level.
### Using a Tag Manager
-The flow could look like this. Every script that sets cookies which require a consent from the visitor is blocked by default.
-Instead, the Consent Banner is shown. Once the visitor updates it's preferences an event is triggered (`cookie_consent_preferences_updated`).
+The flow could look like this: Every script that sets cookies which require a consent from the visitor is blocked by default.
+Instead, the Consent Banner is shown. Once the visitor updates its preferences an event is triggered (`cookie_consent_preferences_updated`).
Additionally the consent data is stored within a cookie in a format that can be parsed either programmatically or with a tag manager (e.g.: name: `cookies_accepted_categories`, value: `technically_required,analytics,marketing`). A tag manager could read the value of that `1st-Party Cookie` before any other script (tag) is loaded (e.g.: Fire trigger only `if Accepted Cookie Categories contains marketing`).
![](./assets/consentFlow.svg)
@@ -51,9 +51,9 @@ Have a look on the Real World example using the React Component: [`packages/cook
## :spiral_notepad: Documentation
-The Cookie Consent Banner supports multiple frontend frameworks, because it is build as an agnostic web component.
+The Cookie Consent Banner supports multiple frontend frameworks, because it is built as an agnostic web component.
For easier integration we also provide a wrapper component for React environments.
-It's necessary to set at least the required properties for the component to work properly (see the provided examples).
+It's necessary to set at least the required properties for the component in order to work properly (see the provided examples).
### Web Component – Vanilla JS
diff --git a/package.json b/package.json
index 6b717ae..d1b0876 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"test:ci": "echo \"Root doesn't have any tests defined.\""
},
"devDependencies": {
+ "@porscheofficial/prettier-config-porschedigital": "2.3.0",
"@types/node": "18.11.18",
"typescript": "4.9.4"
},
diff --git a/packages/cookie-consent-banner-react/package.json b/packages/cookie-consent-banner-react/package.json
index c907146..d9c2ee1 100644
--- a/packages/cookie-consent-banner-react/package.json
+++ b/packages/cookie-consent-banner-react/package.json
@@ -44,7 +44,6 @@
},
"devDependencies": {
"@porscheofficial/eslint-config-porschedigital-react": "2.3.0",
- "@porscheofficial/prettier-config-porschedigital": "2.3.0",
"@types/react": "18.0.27",
"@types/react-dom": "18.0.10",
"eslint": "8.32.0",
diff --git a/packages/cookie-consent-banner/.eslintrc b/packages/cookie-consent-banner/.eslintrc
index 398d886..44fad3d 100644
--- a/packages/cookie-consent-banner/.eslintrc
+++ b/packages/cookie-consent-banner/.eslintrc
@@ -6,5 +6,6 @@
"rules": {
"react/react-in-jsx-scope": "off",
"react/no-unknown-property": "off"
- }
-}
\ No newline at end of file
+ },
+ "plugins": ["eslint-plugin-html"]
+}
diff --git a/packages/cookie-consent-banner/package.json b/packages/cookie-consent-banner/package.json
index 6a2609c..2b981b6 100644
--- a/packages/cookie-consent-banner/package.json
+++ b/packages/cookie-consent-banner/package.json
@@ -18,7 +18,7 @@
"release:prepare": "standard-version",
"test": "stencil test --spec --e2e",
"test:watch": "stencil test --spec --e2e --watchAll",
- "test:ci": "yarn prettier:ci && yarn eslint:ci && stencil test --spec --passWithNoTests"
+ "test:ci": "yarn prettier:ci && yarn eslint:ci && stencil test --spec --e2e --passWithNoTests"
},
"main": "dist/index.cjs.js",
"module": "dist/index.js",
@@ -48,11 +48,11 @@
},
"devDependencies": {
"@porscheofficial/eslint-config-porschedigital-react": "2.3.0",
- "@porscheofficial/prettier-config-porschedigital": "2.3.0",
"@stencil/react-output-target": "0.4.0",
"@types/jest": "27.5.2",
"@types/puppeteer": "5.4.7",
"eslint": "8.32.0",
+ "eslint-plugin-html": "7.1.0",
"jest": "27.5.1",
"jest-cli": "27.5.1",
"prettier": "2.8.3",
diff --git a/packages/cookie-consent-banner/src/components/cookie-consent-banner/cookie-consent-banner.test.ts b/packages/cookie-consent-banner/src/components/cookie-consent-banner/cookie-consent-banner.test.ts
new file mode 100644
index 0000000..c035b49
--- /dev/null
+++ b/packages/cookie-consent-banner/src/components/cookie-consent-banner/cookie-consent-banner.test.ts
@@ -0,0 +1,104 @@
+import { E2EPage, newE2EPage } from "@stencil/core/testing";
+
+const cookieBannerFullyConfigured = `
+
+ We use cookies and similar technologies to provide certain features,
+ enhance the user experience and deliver content that is relevant to your
+ interests. Depending on their purpose, analysis and marketing cookies may
+ be used in addition to technically necessary cookies. By clicking on
+ "Agree and continue", you declare your consent to the use of the
+ aforementioned cookies.
+
+ Here
+
+ you can make detailed settings or revoke your consent (in part if
+ necessary) with effect for the future. For further information, please
+ refer to our
+ Privacy Policy
+ .
+
+`;
+
+describe("Cookie Consent Banner", () => {
+ let page: E2EPage;
+
+ beforeEach(async () => {
+ page = await newE2EPage();
+ });
+
+ it("should render", async () => {
+ await page.setContent(cookieBannerFullyConfigured);
+
+ const cookieBanner = await page.find("cookie-consent-banner");
+
+ expect(cookieBanner).toBeDefined();
+ expect(cookieBanner).toHaveClasses(["hydrated"]);
+ });
+
+ it("should be displayed if no cookies are set", async () => {
+ await page.setContent(cookieBannerFullyConfigured);
+ const cookieBannerInnerDiv = await page.find(
+ "cookie-consent-banner >>> .cc"
+ );
+
+ expect(cookieBannerInnerDiv).toBeDefined();
+ });
+
+ it("should be displayed if cookies other than cookieName are set", async () => {
+ await page.setContent(cookieBannerFullyConfigured);
+ await page.setCookie({
+ name: "someUnrelatedCookie",
+ value: "someValue",
+ domain: "localhost",
+ });
+ const cookieBannerInnerDiv = await page.find(
+ "cookie-consent-banner >>> .cc"
+ );
+
+ expect(cookieBannerInnerDiv).toBeDefined();
+ });
+
+ it("should not be displayed if cookieName cookie is set", async () => {
+ // default `cookieName` is cookies_accepted_categories
+ await page.setCookie({
+ name: "cookies_accepted_categories",
+ value: "technically_required,analytics",
+ domain: "localhost",
+ });
+ await page.setContent(cookieBannerFullyConfigured);
+ const cookieBannerInnerDiv = await page.find(
+ "cookie-consent-banner >>> .cc"
+ );
+
+ expect(cookieBannerInnerDiv).toBeNull();
+ });
+
+ it("should not be displayed if cookieName cookie and other cookies are set", async () => {
+ // default `cookieName` is cookies_accepted_categories
+ await page.setCookie({
+ name: "cookies_accepted_categories",
+ value: "technically_required,analytics",
+ domain: "localhost",
+ });
+
+ await page.setCookie({
+ name: "someUnrelatedCookie",
+ value: "someValue",
+ domain: "localhost",
+ });
+ await page.setContent(cookieBannerFullyConfigured);
+ const cookieBannerInnerDiv = await page.find(
+ "cookie-consent-banner >>> .cc"
+ );
+
+ expect(cookieBannerInnerDiv).toBeNull();
+ });
+});
diff --git a/packages/cookie-consent-banner/src/components/cookie-consent-banner/cookie-consent-banner.tsx b/packages/cookie-consent-banner/src/components/cookie-consent-banner/cookie-consent-banner.tsx
index 1bc2cf2..853b45f 100644
--- a/packages/cookie-consent-banner/src/components/cookie-consent-banner/cookie-consent-banner.tsx
+++ b/packages/cookie-consent-banner/src/components/cookie-consent-banner/cookie-consent-banner.tsx
@@ -11,6 +11,7 @@ import {
JSX,
} from "@stencil/core";
import { CategoryItem } from "./types";
+import { getCookie } from "../../utils/parseCookies";
@Component({
tag: "cookie-consent-banner",
@@ -128,9 +129,7 @@ export class CookieConsentBanner {
let cookieValues: string[] = [];
if (document.cookie) {
- const cookieValueString =
- `; ${document.cookie}`.split(`; ${this.cookieName}=`).pop() ??
- "".split(";").shift();
+ const cookieValueString = getCookie(this.cookieName);
cookieValues = cookieValueString ? cookieValueString.split(",") : [];
}
diff --git a/packages/cookie-consent-banner/src/utils/parseCookies.test.ts b/packages/cookie-consent-banner/src/utils/parseCookies.test.ts
new file mode 100644
index 0000000..5ab9f7d
--- /dev/null
+++ b/packages/cookie-consent-banner/src/utils/parseCookies.test.ts
@@ -0,0 +1,108 @@
+import { parseCookies, CookieMap } from "./parseCookies";
+
+const mockDocumentCookie = (cookieString: string): Document =>
+ Object.defineProperty(window.document, "cookie", {
+ writable: true,
+ value: cookieString,
+ });
+
+describe("parseCookies", () => {
+ it("should work with one cookie", () => {
+ const cookieStringSingle = "first=someValue";
+ const expectedParsed: CookieMap = {
+ first: "someValue",
+ };
+
+ mockDocumentCookie(cookieStringSingle);
+
+ expect(parseCookies()).toMatchObject(expectedParsed);
+ });
+
+ it("should work with multiple cookies", () => {
+ const cookieStringSingle =
+ "first=someValue;second=someValue;third=someValue";
+ const expectedParsed: CookieMap = {
+ first: "someValue",
+ second: "someValue",
+ third: "someValue",
+ };
+
+ mockDocumentCookie(cookieStringSingle);
+
+ expect(parseCookies()).toMatchObject(expectedParsed);
+ });
+
+ it("should trim leading spaces", () => {
+ const cookieStringWithLeadingSpaces = "first=someValue; second=someValue";
+ const expectedParsed: CookieMap = {
+ first: "someValue",
+ second: "someValue",
+ };
+
+ mockDocumentCookie(cookieStringWithLeadingSpaces);
+
+ expect(parseCookies()).toMatchObject(expectedParsed);
+ });
+
+ it("should trim trailing spaces", () => {
+ const cookieStringWithTrailingSpaces = "first=someValue ;second=someValue";
+ const expectedParsed: CookieMap = {
+ first: "someValue",
+ second: "someValue",
+ };
+
+ mockDocumentCookie(cookieStringWithTrailingSpaces);
+
+ expect(parseCookies()).toMatchObject(expectedParsed);
+ });
+
+ it("should trim leading and trailing spaces", () => {
+ const cookieStringLeadingAndTrailingSpaces =
+ " first=someValue ;second=someValue";
+ const expectedParsed: CookieMap = {
+ first: "someValue",
+ second: "someValue",
+ };
+
+ mockDocumentCookie(cookieStringLeadingAndTrailingSpaces);
+
+ expect(parseCookies()).toMatchObject(expectedParsed);
+ });
+
+ it("should trim leading tabs", () => {
+ const cookieStringWithLeadingTabs = "\tfirst=someValue;second=someValue";
+ const expectedParsed: CookieMap = {
+ first: "someValue",
+ second: "someValue",
+ };
+
+ mockDocumentCookie(cookieStringWithLeadingTabs);
+
+ expect(parseCookies()).toMatchObject(expectedParsed);
+ });
+
+ it("should trim trailing tabs", () => {
+ const cookieStringWithTrailingTabs = "first=someValue\t;second=someValue";
+ const expectedParsed: CookieMap = {
+ first: "someValue",
+ second: "someValue",
+ };
+
+ mockDocumentCookie(cookieStringWithTrailingTabs);
+
+ expect(parseCookies()).toMatchObject(expectedParsed);
+ });
+
+ it("should trim leading and trailing tabs", () => {
+ const cookieStringLeadingAndTrailingTabs =
+ "\tfirst=someValue\t;second=someValue";
+ const expectedParsed: CookieMap = {
+ first: "someValue",
+ second: "someValue",
+ };
+
+ mockDocumentCookie(cookieStringLeadingAndTrailingTabs);
+
+ expect(parseCookies()).toMatchObject(expectedParsed);
+ });
+});
diff --git a/packages/cookie-consent-banner/src/utils/parseCookies.ts b/packages/cookie-consent-banner/src/utils/parseCookies.ts
new file mode 100644
index 0000000..6bd9d98
--- /dev/null
+++ b/packages/cookie-consent-banner/src/utils/parseCookies.ts
@@ -0,0 +1,14 @@
+export type CookieMap = Record;
+
+export const parseCookies = (): CookieMap =>
+ document.cookie.split(";").reduce((acc, curr) => {
+ const [key, value] = curr.split("=");
+
+ // key and value may be surrounded by whitespace (space and tab characters)
+ const cookieKey = key.trim();
+ const cookieValue = value.trim();
+ return { ...acc, [cookieKey]: cookieValue };
+ }, {});
+
+export const getCookie = (cookieName: string): string | undefined =>
+ parseCookies()[cookieName];
diff --git a/yarn.lock b/yarn.lock
index d2442d2..a5542bf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -789,7 +789,6 @@ __metadata:
dependencies:
"@porscheofficial/cookie-consent-banner": 3.1.2
"@porscheofficial/eslint-config-porschedigital-react": 2.3.0
- "@porscheofficial/prettier-config-porschedigital": 2.3.0
"@types/react": 18.0.27
"@types/react-dom": 18.0.10
eslint: 8.32.0
@@ -810,12 +809,12 @@ __metadata:
resolution: "@porscheofficial/cookie-consent-banner@workspace:packages/cookie-consent-banner"
dependencies:
"@porscheofficial/eslint-config-porschedigital-react": 2.3.0
- "@porscheofficial/prettier-config-porschedigital": 2.3.0
"@stencil/core": 2.22.1
"@stencil/react-output-target": 0.4.0
"@types/jest": 27.5.2
"@types/puppeteer": 5.4.7
eslint: 8.32.0
+ eslint-plugin-html: 7.1.0
jest: 27.5.1
jest-cli: 27.5.1
prettier: 2.8.3
@@ -2491,6 +2490,24 @@ __metadata:
languageName: node
linkType: hard
+"dom-serializer@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "dom-serializer@npm:2.0.0"
+ dependencies:
+ domelementtype: ^2.3.0
+ domhandler: ^5.0.2
+ entities: ^4.2.0
+ checksum: cd1810544fd8cdfbd51fa2c0c1128ec3a13ba92f14e61b7650b5de421b88205fd2e3f0cc6ace82f13334114addb90ed1c2f23074a51770a8e9c1273acbc7f3e6
+ languageName: node
+ linkType: hard
+
+"domelementtype@npm:^2.3.0":
+ version: 2.3.0
+ resolution: "domelementtype@npm:2.3.0"
+ checksum: ee837a318ff702622f383409d1f5b25dd1024b692ef64d3096ff702e26339f8e345820f29a68bcdcea8cfee3531776b3382651232fbeae95612d6f0a75efb4f6
+ languageName: node
+ linkType: hard
+
"domexception@npm:^2.0.1":
version: 2.0.1
resolution: "domexception@npm:2.0.1"
@@ -2500,6 +2517,26 @@ __metadata:
languageName: node
linkType: hard
+"domhandler@npm:^5.0.1, domhandler@npm:^5.0.2, domhandler@npm:^5.0.3":
+ version: 5.0.3
+ resolution: "domhandler@npm:5.0.3"
+ dependencies:
+ domelementtype: ^2.3.0
+ checksum: 0f58f4a6af63e6f3a4320aa446d28b5790a009018707bce2859dcb1d21144c7876482b5188395a188dfa974238c019e0a1e610d2fc269a12b2c192ea2b0b131c
+ languageName: node
+ linkType: hard
+
+"domutils@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "domutils@npm:3.0.1"
+ dependencies:
+ dom-serializer: ^2.0.0
+ domelementtype: ^2.3.0
+ domhandler: ^5.0.1
+ checksum: 23aa7a840572d395220e173cb6263b0d028596e3950100520870a125af33ff819e6f609e1606d6f7d73bd9e7feb03bb404286e57a39063b5384c62b724d987b3
+ languageName: node
+ linkType: hard
+
"dot-prop@npm:^5.1.0":
version: 5.3.0
resolution: "dot-prop@npm:5.3.0"
@@ -2565,6 +2602,13 @@ __metadata:
languageName: node
linkType: hard
+"entities@npm:^4.2.0, entities@npm:^4.4.0":
+ version: 4.5.0
+ resolution: "entities@npm:4.5.0"
+ checksum: 853f8ebd5b425d350bffa97dd6958143179a5938352ccae092c62d1267c4e392a039be1bae7d51b6e4ffad25f51f9617531fedf5237f15df302ccfb452cbf2d7
+ languageName: node
+ linkType: hard
+
"env-paths@npm:^2.2.0":
version: 2.2.1
resolution: "env-paths@npm:2.2.1"
@@ -2758,6 +2802,15 @@ __metadata:
languageName: node
linkType: hard
+"eslint-plugin-html@npm:7.1.0":
+ version: 7.1.0
+ resolution: "eslint-plugin-html@npm:7.1.0"
+ dependencies:
+ htmlparser2: ^8.0.1
+ checksum: e92b0cf759c8d8aecff8fdea259fdc801fcff775096756d0b5a24f0c846012232fe74eaf44ea9b31b88e695605a8eff437e5fa440a4beed79c35dfaac190db79
+ languageName: node
+ linkType: hard
+
"eslint-plugin-import@npm:2.27.5":
version: 2.27.5
resolution: "eslint-plugin-import@npm:2.27.5"
@@ -3665,6 +3718,18 @@ __metadata:
languageName: node
linkType: hard
+"htmlparser2@npm:^8.0.1":
+ version: 8.0.2
+ resolution: "htmlparser2@npm:8.0.2"
+ dependencies:
+ domelementtype: ^2.3.0
+ domhandler: ^5.0.3
+ domutils: ^3.0.1
+ entities: ^4.4.0
+ checksum: 29167a0f9282f181da8a6d0311b76820c8a59bc9e3c87009e21968264c2987d2723d6fde5a964d4b7b6cba663fca96ffb373c06d8223a85f52a6089ced942700
+ languageName: node
+ linkType: hard
+
"http-cache-semantics@npm:^4.1.0":
version: 4.1.0
resolution: "http-cache-semantics@npm:4.1.0"
@@ -6225,6 +6290,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "root-workspace-0b6124@workspace:."
dependencies:
+ "@porscheofficial/prettier-config-porschedigital": 2.3.0
"@types/node": 18.11.18
typescript: 4.9.4
languageName: unknown