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

Commit

Permalink
Introduce cookieName attribute to allow a custom cookie name (#145)
Browse files Browse the repository at this point in the history
* Add support of cookieName

* Add test story

* Update src/consent-manager-builder/preferences.ts
  • Loading branch information
dmitry-zaets committed May 12, 2021
1 parent f1d56db commit 777862f
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 12 deletions.
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ All the options are supported. The callback function also receives these exports

### ConsentManager

The `ConsentManager` React component is a prebuilt consent manager UI (it's the one we use on https://segment.com) that uses the [ConsentManagerBuilder][] component under the hood. To use it, just mount the component where you want the consent banner to appear and pass in your own custom copy.
The `ConsentManager` React component is a prebuilt consent manager UI (it's the one we use on <https://segment.com>) that uses the [ConsentManagerBuilder][] component under the hood. To use it, just mount the component where you want the consent banner to appear and pass in your own custom copy.

#### Props

Expand Down Expand Up @@ -475,6 +475,13 @@ Default: the [top most domain][top-domain] and all sub domains

The domain the `tracking-preferences` cookie should be scoped to.

##### cookieName

Type: `string`<br>
Default: `tracking-preferences`

The cookie name that should be used to store tracking preferences cookie

#### cookieExpires

Type: `number`<br>
Expand Down Expand Up @@ -629,7 +636,7 @@ The CDN to fetch list of integrations from
To run our storybook locally, simply do:

```
$ yarn dev
yarn dev
```

and the storybook should be opened in your browser. We recommend adding a new story for new features, and testing against existing stories when making bug fixes.
Expand All @@ -639,8 +646,8 @@ and the storybook should be opened in your browser. We recommend adding a new st
This package follows semantic versioning. To publish a new version:

```
$ npm version <new-version>
$ npm publish
npm version <new-version>
npm publish
```

## License
Expand Down
40 changes: 40 additions & 0 deletions src/__tests__/consent-manager-builder/preferences.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ describe('preferences', () => {
})
})

test('loadPreferences(cookieName) returns preferences when cookie exists', () => {
document.cookie =
'custom-tracking-preferences={%22version%22:1%2C%22destinations%22:{%22Amplitude%22:true}%2C%22custom%22:{%22functional%22:true}}'

expect(loadPreferences('custom-tracking-preferences')).toMatchObject({
destinationPreferences: {
Amplitude: true
},
customPreferences: {
functional: true
}
})
})

test('savePreferences() saves the preferences', () => {
const ajsIdentify = sinon.spy()

Expand Down Expand Up @@ -93,4 +107,30 @@ describe('preferences', () => {
// TODO: actually check domain
// expect(document.cookie.includes('domain=example.com')).toBe(true)
})

test('savePreferences() sets the cookie with custom key', () => {
const ajsIdentify = sinon.spy()
// @ts-ignore
window.analytics = { identify: ajsIdentify }
document.cookie = ''

const destinationPreferences = {
Amplitude: true
}

savePreferences({
destinationPreferences,
customPreferences: undefined,
cookieDomain: undefined,
cookieName: 'custom-tracking-preferences'
})

expect(ajsIdentify.calledOnce).toBe(true)
expect(ajsIdentify.args[0][0]).toMatchObject({
destinationTrackingPreferences: destinationPreferences,
customTrackingPreferences: undefined
})

expect(document.cookie.includes('custom-tracking-preferences')).toBe(true)
})
})
25 changes: 20 additions & 5 deletions src/consent-manager-builder/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface Props {
otherWriteKeys?: string[]

cookieDomain?: string
cookieName?: string

/**
* Number of days until the preferences cookie should expire
Expand Down Expand Up @@ -184,11 +185,12 @@ export default class ConsentManagerBuilder extends Component<Props, State> {
mapCustomPreferences,
defaultDestinationBehavior,
cookieDomain,
cookieName,
cookieExpires,
cdnHost = ConsentManagerBuilder.defaultProps.cdnHost
} = this.props
// TODO: add option to run mapCustomPreferences on load so that the destination preferences automatically get updated
let { destinationPreferences, customPreferences } = loadPreferences()
let { destinationPreferences, customPreferences } = loadPreferences(cookieName)

const [isConsentRequired, destinations] = await Promise.all([
shouldRequireConsent(),
Expand Down Expand Up @@ -216,7 +218,13 @@ export default class ConsentManagerBuilder extends Component<Props, State> {
const mapped = mapCustomPreferences(destinations, preferences)
destinationPreferences = mapped.destinationPreferences
customPreferences = mapped.customPreferences
savePreferences({ destinationPreferences, customPreferences, cookieDomain, cookieExpires })
savePreferences({
destinationPreferences,
customPreferences,
cookieDomain,
cookieName,
cookieExpires
})
}
} else {
preferences = destinationPreferences || initialPreferences
Expand Down Expand Up @@ -255,8 +263,8 @@ export default class ConsentManagerBuilder extends Component<Props, State> {
}

handleResetPreferences = () => {
const { initialPreferences, mapCustomPreferences } = this.props
const { destinationPreferences, customPreferences } = loadPreferences()
const { initialPreferences, mapCustomPreferences, cookieName } = this.props
const { destinationPreferences, customPreferences } = loadPreferences(cookieName)

let preferences: CategoryPreferences | undefined
if (mapCustomPreferences) {
Expand All @@ -272,6 +280,7 @@ export default class ConsentManagerBuilder extends Component<Props, State> {
const {
writeKey,
cookieDomain,
cookieName,
cookieExpires,
mapCustomPreferences,
defaultDestinationBehavior
Expand Down Expand Up @@ -309,7 +318,13 @@ export default class ConsentManagerBuilder extends Component<Props, State> {

// If preferences haven't changed, don't reload the page as it's a disruptive experience for end-users
if (prevState.havePreferencesChanged || newDestinations.length > 0) {
savePreferences({ destinationPreferences, customPreferences, cookieDomain, cookieExpires })
savePreferences({
destinationPreferences,
customPreferences,
cookieDomain,
cookieName,
cookieExpires
})
conditionallyLoadAnalytics({
writeKey,
destinations,
Expand Down
12 changes: 9 additions & 3 deletions src/consent-manager-builder/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import topDomain from '@segment/top-domain'
import { WindowWithAJS, Preferences, CategoryPreferences } from '../types'
import { EventEmitter } from 'events'

const DEFAULT_COOKIE_NAME = 'tracking-preferences'
const COOKIE_KEY = 'tracking-preferences'
const COOKIE_DEFAULT_EXPIRES = 365

Expand All @@ -15,8 +16,8 @@ export interface PreferencesManager {

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

if (!preferences) {
return {}
Expand All @@ -28,7 +29,11 @@ export function loadPreferences(): Preferences {
}
}

type SavePreferences = Preferences & { cookieDomain?: string; cookieExpires?: number }
type SavePreferences = Preferences & {
cookieDomain?: string
cookieName?: string
cookieExpires?: number
}

const emitter = new EventEmitter()

Expand All @@ -47,6 +52,7 @@ export function savePreferences({
destinationPreferences,
customPreferences,
cookieDomain,
cookieName,
cookieExpires
}: SavePreferences) {
const wd = window as WindowWithAJS
Expand Down
3 changes: 3 additions & 0 deletions src/consent-manager/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default class ConsentManager extends PureComponent<ConsentManagerProps, {
implyConsentOnInteraction: false,
onError: undefined,
cookieDomain: undefined,
cookieName: undefined,
cookieExpires: undefined,
customCategories: undefined,
bannerTextColor: '#fff',
Expand All @@ -36,6 +37,7 @@ export default class ConsentManager extends PureComponent<ConsentManagerProps, {
shouldRequireConsent,
implyConsentOnInteraction,
cookieDomain,
cookieName,
cookieExpires,
bannerContent,
bannerSubContent,
Expand All @@ -58,6 +60,7 @@ export default class ConsentManager extends PureComponent<ConsentManagerProps, {
otherWriteKeys={otherWriteKeys}
shouldRequireConsent={shouldRequireConsent}
cookieDomain={cookieDomain}
cookieName={cookieName}
cookieExpires={cookieExpires}
initialPreferences={this.getInitialPreferences()}
mapCustomPreferences={this.handleMapCustomPreferences}
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export interface ConsentManagerProps {
shouldRequireConsent?: () => Promise<boolean> | boolean
implyConsentOnInteraction?: boolean
cookieDomain?: string
cookieName?: string
cookieExpires?: number
bannerContent: React.ReactNode
bannerSubContent?: string
Expand Down
140 changes: 140 additions & 0 deletions stories/0.2-consent-manager-custom-cookie-name.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import React from 'react'
import cookies from 'js-cookie'
import { Pane, Heading, Button } from 'evergreen-ui'
import { ConsentManager, openConsentManager, loadPreferences, onPreferencesSaved } from '../src'
import { storiesOf } from '@storybook/react'
import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs'
import SyntaxHighlighter from 'react-syntax-highlighter'
import { Preferences } from '../src/types'
import CookieView from './components/CookieView'

const bannerContent = (
<span>
We use cookies (and other similar technologies) to collect data to improve your experience on
our site. By using our website, you’re agreeing to the collection of data as described in our{' '}
<a
href="https://segment.com/docs/legal/website-data-collection-policy/"
target="_blank"
rel="noopener noreferrer"
>
Website Data Collection Policy
</a>
.
</span>
)
const bannerSubContent = 'You can manage your preferences here!'
const preferencesDialogTitle = 'Website Data Collection Preferences'
const preferencesDialogContent = (
<div>
<p>
Segment uses 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.
</p>
<p>
By using our website, you’re agreeing to our{' '}
<a
href="https://segment.com/docs/legal/website-data-collection-policy/"
target="_blank"
rel="noopener noreferrer"
>
Website Data Collection Policy
</a>
.
</p>
<p>
The table below outlines how we use this data by category. To opt out of a category of data
collection, select “No” and save your preferences.
</p>
</div>
)
const cancelDialogTitle = 'Are you sure you want to cancel?'
const cancelDialogContent = (
<div>
Your preferences have not been saved. By continuing to use our website, you’re agreeing to our{' '}
<a
href="https://segment.com/docs/legal/website-data-collection-policy/"
target="_blank"
rel="noopener noreferrer"
>
Website Data Collection Policy
</a>
.
</div>
)

const ConsentManagerExample = (props: { cookieName: string }) => {
const [prefs, updatePrefs] = React.useState<Preferences>(
loadPreferences('custom-tracking-preferences')
)

const cleanup = onPreferencesSaved(preferences => {
updatePrefs(preferences)
})

React.useEffect(() => {
return () => {
cleanup()
}
})

return (
<Pane>
<ConsentManager
writeKey="tYQQPcY78Hc3T1hXUYk0n4xcbEHnN7r0"
otherWriteKeys={['vMRS7xbsjH97Bb2PeKbEKvYDvgMm5T3l']}
bannerContent={bannerContent}
bannerSubContent={bannerSubContent}
preferencesDialogTitle={preferencesDialogTitle}
preferencesDialogContent={preferencesDialogContent}
cancelDialogTitle={cancelDialogTitle}
cancelDialogContent={cancelDialogContent}
cookieName={props.cookieName}
/>

<Pane marginX={100} marginTop={20}>
<Heading> Your website content </Heading>
<Pane display="flex">
<iframe
src="https://giphy.com/embed/JIX9t2j0ZTN9S"
width="480"
height="480"
frameBorder="0"
/>

<iframe
src="https://giphy.com/embed/yFQ0ywscgobJK"
width="398"
height="480"
frameBorder="0"
/>
</Pane>

<p>
<div>
<Heading>Current Preferences</Heading>
<SyntaxHighlighter language="json" style={docco}>
{JSON.stringify(prefs, null, 2)}
</SyntaxHighlighter>
</div>
<Button marginRight={20} onClick={openConsentManager}>
Change Cookie Preferences
</Button>
<Button
onClick={() => {
cookies.remove('tracking-preferences')
window.location.reload()
}}
>
Clear
</Button>
</p>
</Pane>
<CookieView />
</Pane>
)
}

storiesOf('React Component / Custom Cookie Name', module).add(`Custom Cookie Name`, () => (
<ConsentManagerExample cookieName="custom-tracking-preferences" />
))

0 comments on commit 777862f

Please sign in to comment.