Skip to content

Commit

Permalink
Change default cookie policy keys, add more docs
Browse files Browse the repository at this point in the history
  • Loading branch information
ahosgood committed Sep 15, 2023
1 parent 8705afe commit d4e7c88
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Accept.play = async ({ canvasElement }) => {
const cookies = new Cookies();
deleteAllCookies(cookies);

await expect(cookies.isPolicyAccepted("analytics")).toEqual(false);
await expect(cookies.isPolicyAccepted("usage")).toEqual(false);
await expect(cookies.isPolicyAccepted("settings")).toEqual(false);
await expect(cookies.isPolicyAccepted("unknown")).toEqual(null);

Expand All @@ -71,7 +71,7 @@ Accept.play = async ({ canvasElement }) => {
await expect(acceptButton).toBeVisible();
await userEvent.click(acceptButton);

await expect(cookies.isPolicyAccepted("analytics")).toEqual(true);
await expect(cookies.isPolicyAccepted("usage")).toEqual(true);
await expect(cookies.isPolicyAccepted("settings")).toEqual(true);
await expect(cookies.isPolicyAccepted("unknown")).toEqual(null);
await expect(acceptButton).not.toBeVisible();
Expand All @@ -94,7 +94,7 @@ Reject.play = async ({ canvasElement }) => {
const cookies = new Cookies();
deleteAllCookies(cookies);

await expect(cookies.isPolicyAccepted("analytics")).toEqual(false);
await expect(cookies.isPolicyAccepted("usage")).toEqual(false);
await expect(cookies.isPolicyAccepted("settings")).toEqual(false);
await expect(cookies.isPolicyAccepted("unknown")).toEqual(null);

Expand All @@ -103,7 +103,7 @@ Reject.play = async ({ canvasElement }) => {
await expect(rejectButton).toBeVisible();
await userEvent.click(rejectButton);

await expect(cookies.isPolicyAccepted("analytics")).toEqual(false);
await expect(cookies.isPolicyAccepted("usage")).toEqual(false);
await expect(cookies.isPolicyAccepted("settings")).toEqual(false);
await expect(cookies.isPolicyAccepted("unknown")).toEqual(null);

Expand All @@ -120,15 +120,15 @@ CustomPolicies.play = async ({ args, canvasElement }) => {
const cookies = new Cookies(args.policies.split(","));
deleteAllCookies(cookies);

await expect(cookies.isPolicyAccepted("analytics")).toEqual(null);
await expect(cookies.isPolicyAccepted("usage")).toEqual(null);
await expect(cookies.isPolicyAccepted("settings")).toEqual(null);
await expect(cookies.isPolicyAccepted("custom")).toEqual(false);

const canvas = within(canvasElement);
const acceptButton = canvas.getByText("Accept cookies");
await userEvent.click(acceptButton);

await expect(cookies.isPolicyAccepted("analytics")).toEqual(null);
await expect(cookies.isPolicyAccepted("usage")).toEqual(null);
await expect(cookies.isPolicyAccepted("settings")).toEqual(null);
await expect(cookies.isPolicyAccepted("custom")).toEqual(true);

Expand All @@ -138,7 +138,7 @@ CustomPolicies.play = async ({ args, canvasElement }) => {
export const AddScriptsOnAccept = Template.bind({});
AddScriptsOnAccept.args = {
cookiesUrl: "#",
loadScriptsOnAccept: "my-analytics-script.js",
loadScriptsOnAccept: "my-usage-script.js",
classes: "tna-cookie-banner--demo",
};
AddScriptsOnAccept.play = async ({ args, canvasElement }) => {
Expand Down
2 changes: 1 addition & 1 deletion src/nationalarchives/components/cookie-banner/template.njk
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% from "nationalarchives/components/button/macro.njk" import tnaButton %}

{%- set containerClasses = [params.classes] if params.classes else [] -%}
<div class="tna-cookie-banner {{ containerClasses | join(' ') }}" role="region" aria-label="Cookie usage" data-module="tna-cookie-banner" data-policies="{{ params.policies if params.policies else 'analytics,settings' }}" data-hidekey="{{ params.hideCookieBannerKey if params.hideCookieBannerKey else 'hide_tna_cookie_banner' }}"{% if params.loadScriptsOnAccept %} data-acceptscripts="{{ params.loadScriptsOnAccept }}"{% endif %}{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor %} hidden>
<div class="tna-cookie-banner {{ containerClasses | join(' ') }}" role="region" aria-label="Cookie usage" data-module="tna-cookie-banner" data-policies="{{ params.policies if params.policies else 'usage,settings' }}" data-hidekey="{{ params.hideCookieBannerKey if params.hideCookieBannerKey else 'hide_tna_cookie_banner' }}"{% if params.loadScriptsOnAccept %} data-acceptscripts="{{ params.loadScriptsOnAccept }}"{% endif %}{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor %} hidden>
<div class="tna-container">
<div class="tna-column tna-column--full tna-cookie-banner__message tna-cookie-banner__message--prompt">
<h2>This website uses cookies</h2>
Expand Down
45 changes: 26 additions & 19 deletions src/nationalarchives/lib/cookies.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ export default class Cookies {
#policies = {};

constructor(
policies = ["analytics", "settings"],
policies = ["usage", "settings"],
cookiesPolicyKey = "cookies_policy",
crossDomain = false,
) {
this.cookiesPolicyKey = cookiesPolicyKey;
this.crossDomain = crossDomain;
policies.forEach((policy) => {
this.#policies[policy] = false;
this.#policies[policy.toLowerCase()] = false;
});
this.#policies.essential = true;
this.#getPolicies();
}

Expand Down Expand Up @@ -40,10 +41,6 @@ export default class Cookies {
return this.data;
}

get allPolicies() {
return JSON.parse(this.get(this.cookiesPolicyKey));
}

exists(key) {
return Object.prototype.hasOwnProperty.call(this.data, key);
}
Expand All @@ -56,34 +53,57 @@ export default class Cookies {
return decodeURIComponent(this.data[key]);
}

set(key, value, maxAge = 60 * 60 * 24 * 365, path = "/") {
document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(
value,
)}; SameSite=${
this.crossDomain ? "None" : "Lax"
}; path=${path}; max-age=${maxAge}; Secure`;
this.#getPolicies();
}

delete(key, path = "/") {
this.set(key, "", 0, path);
}

get allPolicies() {
return JSON.parse(this.get(this.cookiesPolicyKey));
}

policy(policy) {
return this.#policies[policy];
}

acceptPolicy(policy) {
this.setPolicy(policy, true);
}

rejectPolicy(policy) {
if (policy === "essential") {
return;
}
this.setPolicy(policy, false);
}

setPolicy(policy, accepted) {
this.#policies = {
...this.#policies,
[policy]: accepted,
essential: true,
};
this.set(this.cookiesPolicyKey, JSON.stringify(this.#policies));
}

acceptAllPolicies() {
Object.keys(this.#policies)
.filter((policy) => policy !== "essential")
.filter((policy) => this.#policies[policy] === false)
.forEach((policy) => this.acceptPolicy(policy));
}

rejectAllPolicies() {
Object.keys(this.#policies)
.filter((policy) => policy !== "essential")
.filter((policy) => this.#policies[policy] === true)
.forEach((policy) => this.rejectPolicy(policy));
}
Expand All @@ -94,17 +114,4 @@ export default class Cookies {
? this.#policies[policy] === true
: null;
}

set(key, value, maxAge = 60 * 60 * 24 * 365, path = "/") {
document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(
value,
)}; SameSite=${
this.crossDomain ? "None" : "Lax"
}; path=${path}; max-age=${maxAge}; Secure`;
this.#getPolicies();
}

policy(policy) {
return this.#policies[policy];
}
}
83 changes: 83 additions & 0 deletions src/nationalarchives/stories/development/cookies.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Meta } from "@storybook/blocks";
import { Mermaid } from 'mdx-mermaid/Mermaid';

<Meta title="Development/Cookies" />

# Cookies handling

The principles of cookie handling are:

- Create cookies on the client side (apart from session cookies)
- Update cookies on the client side (apart from session cookies)
- Reject by default (don't assume acceptance and don't add tracking until after they agree)

## Policies

As standard we should use at least these three classes of cookies:

- `essential` - we don't need to ask permission for these
- `usage` - analytics, tracking, data gathering
- `settings` - configured options for the site (e.g. default results view or static light/dark mode)

## Process

### First visit

<Mermaid chart={`sequenceDiagram
User->>Browser: Request page
Browser->>Server: HTTP request with no cookies
Server->>Browser: Rendered HTML with cookie banner and no analytics
Browser->>User: Accept cookies?
alt Accept
User->>Browser: Accept cookies
Browser->>Browser: Create cookie policy with all accepted
Browser->>Browser: Add analytics code with JavaScript
else Reject
User->>Browser: Reject cookies
Browser->>Browser: Create cookie policy with all rejected
end
opt Next request
User->>Browser: Request page
Browser->>Server: HTTP request with cookie policy
Server->>Browser: Rendered HTML with analytics but no cookie banner
end
`} />

### Repeat visit

<Mermaid chart={`sequenceDiagram
User->>Browser: Request page
Browser->>Server: HTTP request with cookie policy
Server->>Browser: Rendered HTML with analytics but no cookie banner
`} />

## Cookie library

When you load in the tna-frontend JavaScript, it comes with a [cookie library](https://github.com/nationalarchives/tna-frontend/blob/main/src/nationalarchives/lib/cookies.mjs).

This is loaded into the `window` object as `TNAFrontend.Cookies`:

```JavaScript
// Initialise a new Cookie instance
const cookies = new TNAFrontend.Cookies();

// Log all the cookies to the console
console.log(cookies.all);
```

### Functions

- `cookies.all` - Returns all the cookies
- `cookies.exists(key)` - Returns `true` if a cookie exists with the name `key`
- `cookies.hasValue(key, value)` - Returns `true` if the cookie with the name `key` is equal to `value`
- `cookies.get(key)` - Returns the cookie with the name `key`
- `cookies.set(key, value, maxAge, path)` - Set a cookie (max age default is one year, default path is `/`)
- `cookies.delete(key)` - Deletes the cookie with the name `key`
- `cookies.allPolicies` - Returns all the cookie policies
- `cookies.policy(policy)` - Returns the cookie policy with the name `policy`
- `cookies.acceptPolicy(policy)` - Accepts the policy with the name `policy`
- `cookies.rejectPolicy(policy)` - Rejects the policy with the name `policy`
- `cookies.setPolicy(policy, accepted)` - Accepts or rejects the policy with the name `policy` depending on the value of `accepted`
- `cookies.acceptAllPolicies()` - Accepts all policies
- `cookies.rejectAllPolicies()` - Rejects all policies
- `cookies.isPolicyAccepted(policy)` - Returns `true` or `false` depending on whether the policy has been accepted
2 changes: 1 addition & 1 deletion src/nationalarchives/stories/development/using.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ From here, you have access to include the SCSS files from the package so you can

### JavaScript

```js
```JavaScript
// Import all the JavaScript
import "node_modules/@nationalarchives/frontend/nationalarchives/all.mjs";

Expand Down

0 comments on commit d4e7c88

Please sign in to comment.