Skip to content
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## [2.0.2](https://github.com/openmail/system1-cmp/compare/v2.0.1...v2.0.2) (2020-09-02)

### Feat

- [x] Automatically set and persist consent signal if valid TC String present on URLParam `?gdpr_consent`
- [x] Enforce boolean properties in logger

## [2.0.1](https://github.com/openmail/system1-cmp/compare/v2.0.0...v2.0.1) (2020-08-31)

### Refactor
Expand Down
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ TCF 2.0 Consent Management Platform (CMP) UI tool. We are in the process of vali
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->


- [Installation / Use](#installation--use)
- [API](#api)
- [Customized API](#customized-api)
- [init](#init)
- [init](#init)
- [onConsentAllChanged](#onconsentallchanged)
- [offConsentAllChanged](#offconsentallchanged)
- [showConsentTool](#showconsenttool)
- [changeLanguage](#changelanguage)
- [Configuration / Config](#configuration--config)
- [theme](#theme)
- [Initialize from URL Param](#initialize-from-url-param)
- [Background and Resources](#background-and-resources)
- [TODO](#todo)
- [Support Matrix](#support-matrix)
Expand Down Expand Up @@ -90,7 +92,7 @@ Read more about [\_\_tcfapi built-in API](https://github.com/InteractiveAdvertis
- [showConsentTool](#showConsentTool)
- [changeLanguage](#changeLanguage)

#### init
### init

Calling `__tcfapi('init', 2, (store) => {})` will trigger the seed-file or loader to async load the larger CMP UI application. Once loaded, the cmp library calls `init` function to load additional dependencies and render the application.

Expand Down Expand Up @@ -177,7 +179,7 @@ __tcfapi('init', 2, () => {}, {
},
canLog: true, // pixel logging for monitoring and alerting
canDebug: false, // console.logs for dev debugging
narrowedVendors: [1, 2, 3, 4, 5], // only show a select numuber of vendors
narrowedVendors: [1, 2, 3, 4, 5], // only show a select vendors
cookieDomain: '', // which domain to set the euconsent and gdpr_opt_in cookie on
});
```
Expand All @@ -188,6 +190,7 @@ __tcfapi('init', 2, () => {}, {
| `canDebug` | optional boolean | `false` | true enables internal console logging for debugging |
| `baseUrl` | optional string | `./config/2.0` | relative or absolute url to load the global vendor list. Combines with `versionedFilename` to load vendorlist. |
| `versionedFilename` | optional string | `vendor-list.json` | file name of the global vendor list. |
| `narrowedVendors` | optional array | `[]` | Only show select vendors. Example [1,4,5,19] |
| `languageFilename` | optional string | `purposes/purposes-[LANG].json` | file name template for gvl localized purpose json files |
| `translationFilename` | optional string | `translations/translations-[LANG].json` | file name template for custom localized json files for UI layer |
| `cookieDomain` | optional string | empty | manage consent across subdomains. Example `.mysite.com` |
Expand All @@ -208,6 +211,18 @@ Override styling choices using the following properties:
- `secondaryColor`: '#869cc0'
- `featuresColor`: '#d0d3d7'

## Initialize from URL Param

We can leverage the spec provided for [URL-based services to process the TC String when it can't execute JavaScript](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20Consent%20string%20and%20vendor%20list%20formats%20v2.md#how-does-a-url-based-service-process-the-tc-string-when-it-cant-execute-javascript) to pass along consent when domains are owned by the same entity.

Using a URLParam `gdpr_consent` you can pass consent to another domain that is using this CMP.

```
?gdpr_consent=${TC_STRING}
```

The CMP will use `?gdpr_consent` URLParam to automatically persist consent and trigger consent change-events _if there is not already an existing consent signal stored in the EUConsent cookie_.

## Background and Resources

The UI layer is inspired by this [IAB TCF CMP UI Webinar presentation](https://iabeurope.eu/wp-content/uploads/2020/01/2020-01-21-TCF-v2.0-CMP-UI-Webinar.pdf).
Expand All @@ -227,7 +242,6 @@ Following Google's [Additional Consent Mode](https://support.google.com/admanage
- [ ] Layer 3 Purpose Details
- [ ] Layer 2 Vendors
- [ ] Theming
- [ ] Auto-consent using [TC-string-passing](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20Consent%20string%20and%20vendor%20list%20formats%20v2.md#full-tc-string-passing)
- [ ] non-personalized performance and monitoring analytics
- [ ] Validate using the [TCF 2.0 validator extension](https://cmp-validator.consensu.org/chrome-extension/latest/IAB-Europe-CMP-Validator-User-Guide.pdf)
- [ ] Separate polyfill bundle, use babelrc instead of manually importing from core-js
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "system1-cmp",
"version": "2.0.1",
"version": "2.0.2",
"description": "System1 Consent Management Platform for TCF 1.1 GDPR Compliance",
"scripts": {
"clean": "rimraf ./dist",
Expand Down Expand Up @@ -106,7 +106,7 @@
"dependencies": {
"@iabtcf/cmpapi": "^1.1.0-3",
"@iabtcf/core": "^1.1.0-3",
"@s1/dpl": "3.0.16",
"@s1/dpl": "3.0.18",
"classnames": "^2.2.5",
"codemirror": "^5.34.0",
"core-js": "^2.5.3",
Expand Down
6 changes: 5 additions & 1 deletion src/s1/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
/* global __VERSION__ */

export const COOKIES = {
VENDOR_CONSENT: 'euconsent-v2',
PUBLISHER_CONSENT: 'pubconsent-v2',
HAS_CONSENTED_ALL: 'gdpr_opt_in',
};
export const CONSENT_SCREENS = {
STACKS_LAYER1: 1,
PURPOSES_LAYER2: 2,
Expand Down
12 changes: 12 additions & 0 deletions src/s1/lib/config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
export const gdprConsentUrlParam = (() => {
let gdprConsent = '';
if (window && window.location && window.location.search) {
const [, gdprConsentParam] = window.location.search.split('gdpr_consent=');
if (gdprConsentParam) {
gdprConsent = gdprConsentParam.split('&')[0];
}
}
return gdprConsent;
})();

export const theme = {
primaryColor: '#0099ff',
textLinkColor: '#0099ff',
Expand All @@ -18,6 +29,7 @@ export const config = {
ccpaApplies: false,
experimentId: 'control',
gdprApplies: false,
gdprConsentUrlParam,
language: 'en',
narrowedVendors: [],
publisherCountryCode: 'US',
Expand Down
25 changes: 12 additions & 13 deletions src/s1/lib/cookie.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
const PUBLISHER_CONSENT_COOKIE_NAME = 'pubconsent_2.0';
const PUBLISHER_CONSENT_COOKIE_MAX_AGE = 33696000;
import { COOKIES } from '../constants';

const VENDOR_CONSENT_COOKIE_NAME = 'euconsent_2.0';
const VENDOR_CONSENT_COOKIE_MAX_AGE = 33696000;
const { HAS_CONSENTED_ALL, PUBLISHER_CONSENT, VENDOR_CONSENT } = COOKIES;

const HAS_CONTENED_ALL = 'gdpr_opt_in';
const HAS_CONTENED_ALL_MAX_AGE = 33696000;
const PUBLISHER_CONSENT_COOKIE_MAX_AGE = 33696000;
const VENDOR_CONSENT_COOKIE_MAX_AGE = 33696000;
const HAS_CONSENTED_ALL_MAX_AGE = 33696000;

const getCookieDomain = (customCookieDomain) => {
const hostname = (window && window.location && window.location.hostname) || '';
Expand Down Expand Up @@ -36,12 +35,12 @@ function writeCookie({ name, value, maxAgeSeconds, path = '/', domain }) {
}

function readPublisherConsentCookie() {
return readCookie(PUBLISHER_CONSENT_COOKIE_NAME);
return readCookie(PUBLISHER_CONSENT);
}

function writePublisherConsentCookie(value, domain) {
writeCookie({
name: PUBLISHER_CONSENT_COOKIE_NAME,
name: PUBLISHER_CONSENT,
value,
maxAgeSeconds: PUBLISHER_CONSENT_COOKIE_MAX_AGE,
path: '/',
Expand All @@ -56,7 +55,7 @@ function writePublisherConsentCookie(value, domain) {
* @returns Promise resolved with decoded cookie object
*/
function readLocalVendorConsentCookie() {
return readCookie(VENDOR_CONSENT_COOKIE_NAME);
return readCookie(VENDOR_CONSENT);
}

/**
Expand All @@ -68,7 +67,7 @@ function readLocalVendorConsentCookie() {
function writeLocalVendorConsentCookie(value, domain) {
return Promise.resolve(
writeCookie({
name: VENDOR_CONSENT_COOKIE_NAME,
name: VENDOR_CONSENT,
value,
maxAgeSeconds: VENDOR_CONSENT_COOKIE_MAX_AGE,
path: '/',
Expand All @@ -86,14 +85,14 @@ function writeVendorConsentCookie(value, domain) {
}

function readConsentedAllCookie() {
return readCookie(HAS_CONTENED_ALL);
return readCookie(HAS_CONSENTED_ALL);
}

function writeConsentedAllCookie(value, domain) {
writeCookie({
name: HAS_CONTENED_ALL,
name: HAS_CONSENTED_ALL,
value,
maxAgeSeconds: HAS_CONTENED_ALL_MAX_AGE,
maxAgeSeconds: HAS_CONSENTED_ALL_MAX_AGE,
path: '/',
domain,
});
Expand Down
31 changes: 24 additions & 7 deletions src/s1/lib/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export default class Store {
}); // fired after gvl.readyPromise and tcData updated if persisted

onReady() {
const { narrowedVendors, cmpId, cmpVersion, publisherCountryCode } = this.config;
const { narrowedVendors, cmpId, cmpVersion, gdprConsentUrlParam, publisherCountryCode } = this.config;
const { vendors } = this.gvl;

if (narrowedVendors && narrowedVendors.length) {
Expand All @@ -133,12 +133,17 @@ export default class Store {

const tcModel = new TCModel(this.gvl);
let persistedTcModel;
let encodedTCString = cookie.readVendorConsentCookie();
const cookieTCString = cookie.readVendorConsentCookie();
const encodedTCString = cookieTCString || gdprConsentUrlParam;

try {
persistedTcModel = encodedTCString && TCString.decode(encodedTCString);
} catch (e) {
console.error('unable to decode tcstring');
logger(LOG_EVENTS.CMPError, {
message: `storeReadyError: unable to decode TCString from ${
gdprConsentUrlParam ? 'consentUrl' : 'consentCookie'
}`,
});
}

// Merge persisted model into new model in memory
Expand All @@ -150,7 +155,7 @@ export default class Store {
consentScreen: CONSENT_SCREENS.STACKS_LAYER1,
});

// Handle a return user with persistedConsent vs a user that has not saved preferences
// Handle a new user
if (!persistedTcModel) {
tcModel.setAllVendorLegitimateInterests();
tcModel.setAllPurposeLegitimateInterests();
Expand All @@ -161,7 +166,11 @@ export default class Store {

// update internal models, show ui, dont save to cookie
this.updateCmp({ tcModel, shouldShowModal: true });
this.setDisplayLayer1();
} else {
// handle a return user

// Note: commented out because automatic vendor consent management creates unexpected result for user.
// update the manually managed vendor consent model set since it's primarily automatically managed
// this is a list of vendor consents that were likely manually revoked by the user
// const { vendorConsents } = tcModel;
Expand All @@ -170,10 +179,16 @@ export default class Store {
// this.manualVendorConsents.add(parseInt(key, 10));
// }
// });
// update internal models, dont show the ui, dont save to cookie

// update internal models, dont show the ui, save to cookie if persisting from URL
this.updateCmp({ tcModel });
this.setDisplayLayer1();

// trigger cookie storage when transfering consent from URL
if (gdprConsentUrlParam && !cookieTCString) {
this.updateCmp({ tcModel, shouldSaveCookie: true, isConsentByUrl: true });
}
}
this.setDisplayLayer1();
}

onEvent(tcData, success) {
Expand Down Expand Up @@ -207,8 +222,9 @@ export default class Store {
* @oaram tcModelOpt - optional ModelObject, updates to the tcModel
* @param shouldShowModal - optional boolean, displays UI if true
* @param shouldSaveCookie - optional boolean, sets gdpr_opt_in and stores tcData.consentString too cookie if true
* @param isConsentByUrl - optional boolean, annotates logs to indicate save of consent transfered from URLParams
*/
updateCmp = ({ tcModel, shouldShowModal, shouldSaveCookie, shouldShowSave }) => {
updateCmp = ({ tcModel, shouldShowModal, shouldSaveCookie, shouldShowSave, isConsentByUrl = false }) => {
const tcModelNew = this.autoToggleVendorConsents(tcModel);
const isModalShowing = shouldShowModal !== undefined ? shouldShowModal : this.isModalShowing;
const isSaveShowing = shouldShowSave !== undefined ? shouldShowSave : this.isSaveShowing;
Expand Down Expand Up @@ -267,6 +283,7 @@ export default class Store {
logger(LOG_EVENTS.CMPSave, {
consentScreen,
hasConsentedAll,
consentByUrl: isConsentByUrl,
declinedStack: this.getStackOptin(stack) ? '' : stack,
declinedPurposes: declinedPurposes.join(','),
declinedSpecialFeatures: declinedSpecialFeatures.join(','),
Expand Down
2 changes: 1 addition & 1 deletion src/s1/reference/tcf-2.0.html
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ <h3>TCFString</h3>
// polyfillSrc: './polyfills.js',
publisherCountryCode: 'US',
language: 'en', // default
narrowedVendors: [],
narrowedVendors: [1, 2, 3, 4, 5, 6],
};

__tcfapi('addEventListener', 2, function (tcData, success) {
Expand Down
4 changes: 2 additions & 2 deletions src/s1/tcf-2.0-cmp.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export const setup = (configOpt) => {
url: global && global.location && global.location.href ? global.location.href.split('?')[0] : 'unknown',
experimentId: config.experimentId,
business: config.business,
ccpaApplies: config.ccpaApplies,
gdprApplies: config.gdprApplies,
ccpaApplies: config.ccpaApplies === true,
gdprApplies: config.gdprApplies === true,
};

logger(LOG_EVENTS.CMPSetupStart || 'CMPSetupStart');
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -969,10 +969,10 @@
"@types/istanbul-reports" "^1.1.1"
"@types/yargs" "^13.0.0"

"@s1/dpl@3.0.16":
version "3.0.16"
resolved "https://system1.jfrog.io/system1/api/npm/npm-virtual/@s1/dpl/-/@s1/dpl-3.0.16.tgz#f135cd8958f05cb15fdc8659364bb4d1a0d2362b"
integrity sha1-8TXNiVjwXLFf3IZZNku00aDSNis=
"@s1/dpl@3.0.18":
version "3.0.18"
resolved "https://system1.jfrog.io/system1/api/npm/npm-virtual/@s1/dpl/-/@s1/dpl-3.0.18.tgz#a635d5e4015cda569c8699794b33133c1adfec2b"
integrity sha1-pjXV5AFc2lachpl5SzMTPBrf7Cs=
dependencies:
"@babel/runtime" "^7.10.5"
doctoc "^1.4.0"
Expand Down