Skip to content
This repository has been archived by the owner on Apr 3, 2024. It is now read-only.

⚡️ Release Consent Manager v4.0.0 #54

Merged
merged 7 commits into from
Oct 10, 2019
Merged
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
1 change: 1 addition & 0 deletions .storybook/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const pkg = require('../package.json')

module.exports = {
mode: 'development',
devtool: 'source-map',
resolve: {
extensions: ['.tsx', '.ts', '.js']
},
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Changelog

## 4.0.0(Oct 10, 2019)

### Breaking

- [#51](https://github.com/segmentio/consent-manager/pull/51) Deprecate data attributes and dataset

### Added

- [#48](https://github.com/segmentio/consent-manager/pull/48) Add new `closeBehavior` option
- [#49](https://github.com/segmentio/consent-manager/pull/49) Initial Preferences override
- [#52](https://github.com/segmentio/consent-manager/pull/52) Expose preferences manager

## 3.0.0(Oct 8, 2019)

### Breaking

## 2.0.0

### Added

- [#46](https://github.com/segmentio/consent-manager/pull/46) ⚡️ Modernize
- [#47](https://github.com/segmentio/consent-manager/pull/47) 🙅🏻‍♀️No longer imply consent on interaction

## 1.3.1(Sep 24, 2019)

### Fixed

- [86387e6](https://github.com/segmentio/consent-manager/commit/86387e63f259fff9f34ee511b2fa6218341dfa17) Fix integrity hash
48 changes: 24 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,27 +68,10 @@ The following global variables are also exposed:
- `consentManager.openConsentManager()` - Opens the consent manager preferences dialog.
- `consentManager.doNotTrack()` - Utility function that returns the user's Do Not Track preference (normalises the cross browser API differences). Returns `true`, `false` or `null` (no preference specified).
- `consentManager.inEU()` - The [@segment/in-eu][ineu] `inEU()` function.

#### Data Attributes

The `shouldRequireConsent` option isn't supported and the `otherWriteKeys` option should be a comma separated list.

_Note: the data attributes [won't work in Internet Explorer][currentscript] (Edge works fine though)._

```html
<script
src="https://unpkg.com/@segment/consent-manager@3.0.0/standalone/consent-manager.js"
defer
data-container="#target-container"
data-writeKey="<your-segment-write-key>"
data-bannerContent="We use cookies (and other similar technologies) to collect data to improve your experience on our site."
data-bannerSubContent="You can change your preferences at any time."
data-preferencesDialogTitle="Website Data Collection Preferences"
data-preferencesDialogContent="We use data collected by cookies and JavaScript libraries to improve your browsing experience, analyze site traffic, deliver personalized advertisements, and increase the overall performance of our site."
data-cancelDialogTitle="Are you sure you want to cancel?"
data-cancelDialogContent="Your preferences have not been saved. By continuing to use our website, you՚re agreeing to our Website Data Collection Policy."
></script>
```
- `consentManager.preferences` - Returns an instance of `PreferencesManager` with the following helper functions:
- `loadPreferences` - returns the cookie value for consent preferences
- `savePreferences` - allows for managing the consent cookie programatically (useful if you want to re-hydrate consent from your own database or prefill consent options)
- `onPreferencesSaved(callback)` - allows for subscribing to changes in preferences.

#### Callback Function

Expand All @@ -99,6 +82,10 @@ All the options are supported. The callback function also receives these exports
- `openConsentManager()` - Opens the consent manager preferences dialog.
- `doNotTrack()` - Utility function that returns the user's Do Not Track preference (normalises the cross browser API differences). Returns `true`, `false` or `null` (no preference specified).
- `inEU()` - The [@segment/in-eu][ineu] `inEU()` function.
- `consentManager.preferences` - Returns an instance of `PreferencesManager` with the following helper functions:
- `loadPreferences` - returns the cookie value for consent preferences
- `savePreferences` - allows for managing the consent cookie programatically (useful if you want to re-hydrate consent from your own database or prefill consent options)
- `onPreferencesSaved(callback)` - allows for subscribing to changes in preferences.

```html
<script>
Expand Down Expand Up @@ -139,9 +126,9 @@ All the options are supported. The callback function also receives these exports
}
}
</script>

<script
src="https://unpkg.com/@segment/consent-manager@3.0.0/standalone/consent-manager.js"
crossorigin="anonymous"
src="https://unpkg.com/@segment/consent-manager@4.0.0/standalone/consent-manager.js"
defer
></script>
```
Expand Down Expand Up @@ -172,6 +159,19 @@ Default: `() => true`

Callback function that determines if consent is required before tracking can begin. Return `true` to show the consent banner and wait for consent (if no consent has been given yet). Return `false` to not show the consent banner and start tracking immediately (unless the user has opted out). The function can return a `Promise` that resolves to a boolean.

##### closeBehavior

Type: `enum|string`<br>
Default: `dismiss`

An option to determine what should be the default behavior for the `x` button on the consent manager banner.

Options:

- `dismiss` (default) - Dismisses the banner, but don't save or change any preferences. Analytics.js won't be loaded until explicit consent is given.
- `accept` - Assume consent across every category.
- `deny` - Denies consent across every category.

##### implyConsentOnInteraction

**_ Breaking Change _** (versions < 3.0.0 will default this option `true`)
Expand Down Expand Up @@ -331,7 +331,7 @@ The initial value of the preferences. By default it should be an object map of `
Type: `function`<br>
Default: `undefined`

Callback function allows you to use a custom preferences format (e.g: categories) instead of the default destination based one. The function gets called during the consent saving process and gets passed `{destinations, preferences}`. The function should return `{destinationPreferences, customPreferences}` where `destinationPreferences` is your custom preferences mapped to the destinations format (`{destiantionId: true|false}`) and `customPreferences` is your custom preferences if you changed them in the callback (optional).
Callback function allows you to use a custom preferences format (e.g: categories) instead of the default destination based one. The function gets called during the consent saving process and gets passed `(destinations, preferences)`. The function should return `{destinationPreferences, customPreferences}` where `destinationPreferences` is your custom preferences mapped to the destinations format (`{destiantionId: true|false}`) and `customPreferences` is your custom preferences if you changed them in the callback (optional).

##### cookieDomain

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@segment/consent-manager",
"version": "3.0.0",
"version": "4.0.0",
"description": "Drop-in consent management plugin for analytics.js",
"keywords": [
"gdpr",
Expand All @@ -22,7 +22,7 @@
"test": "jest src/__tests__",
"prepublishOnly": "yarn run clean && yarn run build",
"postpublish": "yarn build-storybook && yarn deploy-storybook -- --existing-output-dir=storybook-static",
"dev": "yarn build-standalone && start-storybook -s ./ -p 9009",
"dev": "concurrently 'yarn build-standalone --watch' 'yarn start-storybook -s ./ -p 9009'",
"build-commonjs": "tsc --outDir commonjs --inlineSourceMap",
"build-esm": "tsc --module es2015 --outDir esm --inlineSourceMap",
"build-standalone": "webpack",
Expand Down Expand Up @@ -119,7 +119,7 @@
"size-limit": [
{
"path": "esm/index.js",
"limit": "48 KB"
"limit": "50 KB"
},
{
"path": "standalone/consent-manager.js",
Expand Down
2 changes: 1 addition & 1 deletion src/consent-manager-builder/fetch-destinations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async function fetchDestinationForWriteKey(writeKey: string): Promise<Destinatio
}

export default async function fetchDestinations(writeKeys: string[]): Promise<Destination[]> {
const destinationsRequests: any[] = []
const destinationsRequests: Promise<Destination[]>[] = []
for (const writeKey of writeKeys) {
destinationsRequests.push(fetchDestinationForWriteKey(writeKey))
}
Expand Down
67 changes: 49 additions & 18 deletions src/consent-manager-builder/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,41 @@ function getNewDestinations(destinations: Destination[], preferences: CategoryPr
}

interface Props {
onError?: (err: Error) => void | Promise<void>
/** Your Segment Write key for your website */
writeKey: string

/** A list of other write keys you may want to provide */
otherWriteKeys?: string[]
shouldRequireConsent?: () => Promise<boolean> | boolean
initialPreferences?: CategoryPreferences

cookieDomain?: string

/**
* An initial selection of Preferences
*/
initialPreferences?: CategoryPreferences

/**
* Provide a function to define whether or not consent should be required
*/
shouldRequireConsent?: () => Promise<boolean> | boolean

/**
* Render props for the Consent Manager builder
*/
children: (props: RenderProps) => React.ReactElement

mapCustomPreferences?: (args: {
destinations: Destination[]
/**
* Allows for customizing how to show different categories of consent.
*/
mapCustomPreferences?: (
destinations: Destination[],
preferences: CategoryPreferences
}) => { destinationPreferences: CategoryPreferences; customPreferences: CategoryPreferences }
) => { destinationPreferences: CategoryPreferences; customPreferences: CategoryPreferences }

/**
* A callback for dealing with errors in the Consent Manager
*/
onError?: (err: Error) => void | Promise<void>
}

interface RenderProps {
Expand Down Expand Up @@ -113,7 +136,7 @@ export default class ConsentManagerBuilder extends Component<Props, State> {
mapCustomPreferences
} = this.props
// TODO: add option to run mapCustomPreferences on load so that the destination preferences automatically get updated
const { destinationPreferences = {}, customPreferences } = loadPreferences()
let { destinationPreferences = {}, customPreferences } = loadPreferences()

const [isConsentRequired, destinations] = await Promise.all([
shouldRequireConsent(),
Expand All @@ -122,20 +145,31 @@ export default class ConsentManagerBuilder extends Component<Props, State> {

const newDestinations = getNewDestinations(destinations, destinationPreferences)

let preferences: CategoryPreferences | undefined
if (mapCustomPreferences) {
preferences = customPreferences || initialPreferences || {}

const hasInitialPreferenceToTrue = Object.values(initialPreferences || {}).some(Boolean)
const emptyCustomPreferecences = Object.values(customPreferences || {}).every(
v => v === null || v === undefined
)

if (hasInitialPreferenceToTrue && emptyCustomPreferecences) {
const mapped = mapCustomPreferences(destinations, preferences)
destinationPreferences = mapped.destinationPreferences
customPreferences = mapped.customPreferences
}
} else {
preferences = destinationPreferences || initialPreferences
}

conditionallyLoadAnalytics({
writeKey,
destinations,
destinationPreferences,
isConsentRequired
})

let preferences: CategoryPreferences | undefined
if (mapCustomPreferences) {
preferences = customPreferences || initialPreferences
} else {
preferences = destinationPreferences || initialPreferences
}

this.setState({
isLoading: false,
destinations,
Expand Down Expand Up @@ -187,10 +221,7 @@ export default class ConsentManagerBuilder extends Component<Props, State> {
let customPreferences: CategoryPreferences | undefined

if (mapCustomPreferences) {
const custom = mapCustomPreferences({
destinations,
preferences
})
const custom = mapCustomPreferences(destinations, preferences)
destinationPreferences = custom.destinationPreferences
customPreferences = custom.customPreferences

Expand Down
27 changes: 27 additions & 0 deletions src/consent-manager-builder/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@
import cookies from 'js-cookie'
import topDomain from '@segment/top-domain'
import { WindowWithAJS, Preferences, CategoryPreferences } from '../types'
import { EventEmitter } from 'events'

const COOKIE_KEY = 'tracking-preferences'
// TODO: Make cookie expiration configurable
const COOKIE_EXPIRES = 365

export interface PreferencesManager {
loadPreferences(): Preferences
onPreferencesSaved(listener: (prefs: Preferences) => void): void
savePreferences(prefs: SavePreferences): void
}

// TODO: harden against invalid cookies
// TODO: harden against different versions of cookies
export function loadPreferences(): Preferences {
const preferences = cookies.getJSON(COOKIE_KEY)

Expand All @@ -22,6 +31,19 @@ export function loadPreferences(): Preferences {

type SavePreferences = Preferences & { cookieDomain?: string }

const emitter = new EventEmitter()

/**
* Subscribes to consent preferences changing over time and returns
* a cleanup function that can be invoked to remove the instantiated listener.
*
* @param listener a function to be invoked when ConsentPreferences are saved
*/
export function onPreferencesSaved(listener: (prefs: Preferences) => void) {
emitter.on('preferencesSaved', listener)
return () => emitter.off('preferencesSaved', listener)
}

export function savePreferences({
destinationPreferences,
customPreferences,
Expand All @@ -47,4 +69,9 @@ export function savePreferences({
expires: COOKIE_EXPIRES,
domain
})

emitter.emit('preferencesSaved', {
destinationPreferences,
customPreferences
})
}
11 changes: 3 additions & 8 deletions src/consent-manager/banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const CloseButton = styled('button')`

interface Props {
innerRef: (node: HTMLElement | null) => void
onAccept: () => void
onClose: () => void
onChangePreferences: () => void
content: React.ReactNode
subContent: React.ReactNode
Expand All @@ -66,7 +66,7 @@ export default class Banner extends PureComponent<Props> {
render() {
const {
innerRef,
onAccept,
onClose,
onChangePreferences,
content,
subContent,
Expand All @@ -85,12 +85,7 @@ export default class Banner extends PureComponent<Props> {
</P>
</Content>

<CloseButton
type="button"
title="Accept policy"
aria-label="Accept policy"
onClick={onAccept}
>
<CloseButton type="button" title="Close" aria-label="Close" onClick={onClose}>
</CloseButton>
</Root>
Expand Down
Loading