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

New privacy-footer-localiser component #842

Closed
wants to merge 12 commits into from
18 changes: 18 additions & 0 deletions examples/ft-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,22 @@

This example demonstrates all the FT.com layout which includes the header, navigation, and footer components. It uses [Sucrase] to provide support for JSX syntax and ESM.

It also shows how to use the utils exposed by privacy-footer-localiser on the client-side.

[Sucrase]: https://github.com/alangpierce/sucrase

## Comments on `privacy-footer-localiser`

Any of the two methods imported from `dotcom-privacy-footer-localiser` could well be called from `dotcom-ui-footer` if they needed to be applied to footer of all pages.
Currently, these methods will only change the defaut footer if the user is found to be in California by the `compliance-region` service.
The `compliance-region` service doesn't support CORS and it saves the user compliance-region in sessionStorage. Therefore, in order to get this example to make client-side changes to the footer, the following needs to happen:

- The example is rendered from `local.ft.com` with a clean sessionStorage
- The user is located in California or uses a VPN to pretend to be there

Alternatively a record can be saved in sessionStorage with:

```
key: user-compliance
value: {"region":"US-CA","legislation":"ccpa,gdpr"}
```
10 changes: 10 additions & 0 deletions examples/ft-ui/client/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import readyState from 'ready-state'
import * as layout from '@financial-times/dotcom-ui-layout'
import {
addDNSLinkToFooter,
adaptPrivacyLinkToLegislation
} from '@financial-times/dotcom-privacy-footer-localiser'

readyState.domready.then(() => {
layout.init()

// These methods won't perform any changes to the footer unless the user is
// deemed to be in California.
// See this example's README for details on how to fake that.
addDNSLinkToFooter()
adaptPrivacyLinkToLegislation()
})
1 change: 1 addition & 0 deletions examples/ft-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"dependencies": {
"@financial-times/dotcom-middleware-navigation": "file:../../packages/dotcom-middleware-navigation",
"@financial-times/dotcom-privacy-footer-localiser": "file:../../packages/dotcom-privacy-footer-localiser",
"@financial-times/dotcom-ui-layout": "file:../../packages/dotcom-ui-layout",
"@financial-times/dotcom-ui-shell": "file:../../packages/dotcom-ui-shell",
"express": "^4.16.2",
Expand Down
44 changes: 44 additions & 0 deletions packages/dotcom-privacy-footer-localiser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# @financial-times/dotcom-privacy-footer-localiser

This package provides utilities to adapt the default footer of dotcom pages based on the specific requirements of the legislation that applies to each user.

This module relies on `@financial-times/privacy-legislation-client` for determining the legislation that applies to the user.


## Getting started

This package is compatible with Node 12+ and is distributed on npm.

```bash
npm install --save @financial-times/dotcom-privacy-footer-localiser
```

### Server-side

This module doesn't export any server-side functionality.


### Client-side

This package exports two methods to manipulate the footer

- `addDNSLinkToFooter`: adds a "Do Not Sell My Info" link above the "Privacy" if the CCPA legislation applies to the user.
- `adaptPrivacyLinkToLegislation`: replaces the text of the "Privacy" link according to the legislation that applies to the user, if needed.

Neither of those two methods accept any parameters. Both methods rely on the presence of a DOM element that matches the selector: `#site-footer [href='http://help.ft.com/help/legal-privacy/privacy/']`

#### Examples

```js
import { addDNSLinkToFooter } from '@financial-times/dotcom-privacy-footer-localiser'

// ... JS operations with higher priority
addDNSLinkToFooter()
```

```js
import { adaptPrivacyLinkToLegislation } from '@financial-times/dotcom-privacy-footer-localiser'

// ... JS operations with higher priority
adaptPrivacyLinkToLegislation()
```
32 changes: 32 additions & 0 deletions packages/dotcom-privacy-footer-localiser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@financial-times/dotcom-privacy-footer-localiser",
"version": "0.0.0",
"description": "",
"browser": "src/main.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"tsc": "../../node_modules/.bin/tsc --incremental",
"clean": "npm run clean:dist && npm run clean:node_modules",
"clean:dist": "rm -rf dist",
"clean:node_modules": "rm -rf node_modules",
"clean:install": "npm run clean && npm i",
"build": "npm run build:node",
"build:node": "npm run tsc -- --module commonjs --outDir ./dist/node",
"dev": "npm run build:node -- --watch"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@financial-times/privacy-legislation-client": "^0.3.7"
},
"engines": {
"node": ">= 12.0.0"
},
"repository": {
"type": "git",
"repository": "https://github.com/Financial-Times/dotcom-page-kit.git",
"directory": "packages/dotcom-privacy-footer-localiser"
},
"homepage": "https://github.com/Financial-Times/dotcom-page-kit/tree/master/packages/dotcom-privacy-footer-localiser"
}
114 changes: 114 additions & 0 deletions packages/dotcom-privacy-footer-localiser/src/__test__/main.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* @jest-environment jsdom
*/
import 'jest-enzyme'
import React from 'react'
import { mount } from 'enzyme'
import { addDNSLinkToFooter, adaptPrivacyLinkToLegislation } from '../main'

const consentPagePath = 'foo'

jest.mock('@financial-times/privacy-legislation-client', () => {
return {
fetchLegislation: jest.fn().mockResolvedValue({ legislation: new Set(['ccpa', 'gdpr']) }),
buildConsentPageUrl: jest.fn(() => consentPagePath)
}
})

const wait = (ms) => setTimeout(() => Promise.resolve(), ms)

const ValidFooter = () => (
<div>
<div id="site-footer">
<a href="http://help.ft.com/help/legal-privacy/privacy/">Privacy</a>
</div>
</div>
)

const InvalidFooter = () => (
<div>
<div id="site-footer">
<a href="http://help.ft.com/help/legal-privacy/something-else/">Privacy</a>
</div>
</div>
)

describe('dotcom-privacy-footer-localiser', () => {
describe('the addDNSLinkToFooter method', () => {
describe('if there is a Privacy link in the footer', () => {
let links

beforeAll(async () => {
document.body.innerHTML = ''
const div = document.createElement('div')
document.body.appendChild(div)
mount(<ValidFooter />, { attachTo: document.querySelector('div') })
await addDNSLinkToFooter()
links = document.querySelectorAll('a')
})

it('should insert a new link', () => {
expect(links.length).toBe(2)
})

it('should insert the new link before the privacy link', () => {
expect(links[0].href).toMatch(consentPagePath)
})

it('should insert the new link with the right label', () => {
expect(links[0].text).toBe('Do Not Sell My Info')
})
})

describe('if there is no Privacy link in the footer', () => {
it('should call console.error', async () => {
const consoleErr = window.console.error
window.console.error = jest.fn()
const div = document.createElement('div')
document.body.appendChild(div)
mount(<InvalidFooter />, { attachTo: document.querySelector('div') })
await addDNSLinkToFooter()
expect(window.console.error).toHaveBeenCalled()
window.console.error = consoleErr
})
})
})

describe('the adaptPrivacyLinkToLegislation method', () => {
describe('if there is a Privacy link in the footer', () => {
let links

beforeAll(async () => {
document.body.innerHTML = ''
const div = document.createElement('div')
document.body.appendChild(div)
mount(<ValidFooter />, { attachTo: document.querySelector('div') })
await wait(1000)
await adaptPrivacyLinkToLegislation()
await wait(1000)
links = document.querySelectorAll('a')
})

it('should NOT insert a new link', () => {
expect(links.length).toBe(1)
})

it('should change the link text to the right label', () => {
expect(links[0].text).toBe('Privacy - CCPA UPDATES')
})
})

describe('if there is no Privacy link in the footer', () => {
it('should call console.error', async () => {
const consoleErr = window.console.error
window.console.error = jest.fn()
const div = document.createElement('div')
document.body.appendChild(div)
mount(<InvalidFooter />, { attachTo: document.querySelector('div') })
await adaptPrivacyLinkToLegislation()
expect(window.console.error).toHaveBeenCalled()
window.console.error = consoleErr
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { privacyLinkOnFooter } from './selectors'

export function createCCPALink(consentPageUrl: string): void {
// Get a reference to the node we want to insert our link before
const termsLink = document.querySelector(privacyLinkOnFooter) as HTMLAnchorElement

if (!termsLink || !termsLink.parentNode) {
throw new Error('A DOM node for Privacy link could not be found')
}

// Clone the node to keep consistent with structure and style
const ccpaLink = termsLink.cloneNode(true) as HTMLAnchorElement
const ccpaLabel = 'Do Not Sell My Info'

// Customise the attributes
ccpaLink.href = consentPageUrl
ccpaLink.dataset.trackable = ccpaLabel
ccpaLink.textContent = ccpaLabel

// Prepend our new link
const parent = termsLink.parentNode
parent.insertBefore(ccpaLink, termsLink)
}

export function changePrivacyLinkText(newText: string): void {
// Get a reference to the node we want to insert our link before
const termsLink = document.querySelector(privacyLinkOnFooter) as HTMLAnchorElement

if (!termsLink) {
throw new Error('A Privacy link could not be found in the footer')
}

termsLink.innerHTML = newText
}
33 changes: 33 additions & 0 deletions packages/dotcom-privacy-footer-localiser/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { fetchLegislation, buildConsentPageUrl } from '@financial-times/privacy-legislation-client'
import { createCCPALink, changePrivacyLinkText } from './footer-manipulations'

const CONSENT_URL = 'https://www.ft.com/preferences/privacy-ccpa'

export async function addDNSLinkToFooter(): Promise<void> {
try {
// Get a list of the applicable legislationIds for the user's region
const { legislation } = await fetchLegislation()
const consentPageUrl = buildConsentPageUrl({ url: CONSENT_URL, legislation })

// If the user is in California update our UI to meet CCPA requirements
if (legislation.has('ccpa')) {
createCCPALink(consentPageUrl)
}
} catch (err) {
console.error(err) //eslint-disable-line no-console
}
}

export async function adaptPrivacyLinkToLegislation(): Promise<void> {
try {
// Get a list of the applicable legislationIds for the user's region
const { legislation } = await fetchLegislation()

// If the user is in California update our UI to meet CCPA requirements
if (legislation.has('ccpa')) {
changePrivacyLinkText('Privacy - CCPA UPDATES')
}
} catch (err) {
console.error(err) //eslint-disable-line no-console
}
}
1 change: 1 addition & 0 deletions packages/dotcom-privacy-footer-localiser/src/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const privacyLinkOnFooter = "#site-footer [href='http://help.ft.com/help/legal-privacy/privacy/']"
4 changes: 4 additions & 0 deletions packages/dotcom-privacy-footer-localiser/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"rootDir": "./src/",
"extends": "../../tsconfig"
}