@dpgradio/creative 7.1.0 → 7.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -2
- package/package.json +1 -1
- package/src/analytics/dataLayer.js +22 -2
- package/src/config/config.js +3 -3
- package/src/privacy/privacy.js +48 -14
- package/src/utils/loadScript.js +4 -1
package/README.md
CHANGED
|
@@ -61,11 +61,11 @@ await configuration.retrieveConfigForDetectedStation(appId) // Default, by hostn
|
|
|
61
61
|
// Or, if you want to retrieve the config by hostnames only.
|
|
62
62
|
await configuration.retrieveConfigByHostname(appId)
|
|
63
63
|
// Or, if you want to retrieve the config for a specific station:
|
|
64
|
-
await configuration.
|
|
64
|
+
await configuration.retrieveConfigForStation(stationId, appId)
|
|
65
65
|
|
|
66
66
|
// By default the first station ID (stationIdA) is used as the current station of the configuration.
|
|
67
67
|
// If you want to use a different station, you can do so by calling setStation:
|
|
68
|
-
await configuration.
|
|
68
|
+
await configuration.retrieveConfigForStations([stationIdA, stationIdB, stationIdC], appId)
|
|
69
69
|
configuration.setStation(stationIdC)
|
|
70
70
|
```
|
|
71
71
|
You can change the station later on at any time without having to retrieve the config again.
|
|
@@ -366,6 +366,25 @@ const image = new ImageGeneratorProperties('https://static.qmusic.be/acties/joe-
|
|
|
366
366
|
(await shareable.generateUsingImage(image)).openWhatsappUrl()
|
|
367
367
|
```
|
|
368
368
|
|
|
369
|
+
## CSP
|
|
370
|
+
|
|
371
|
+
Sometimes it's necessary to set strict Content Security Policy (CSP) headers. When that's the case a nicety of some of the methods in this package is that they support a random nonce to be passed in so extra sources (GTM, Privacy gate) don't need to be explicitly whitelisted (since they might be unknown at the time of development). Our datalayer and privacy gate initializers have an optional `nonce` parameter that can be passed in.
|
|
372
|
+
|
|
373
|
+
```js
|
|
374
|
+
import { privacy, dataLayer } from '@dpgradio/creative'
|
|
375
|
+
|
|
376
|
+
privacy.initialize(privacyManagerId, websiteUrl, cmpCname, { nonce: "abc1234" })
|
|
377
|
+
dataLayer.initialize({ nonce: "abc1234" })
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Which then allows you to whitelist that nonce in your CSP header
|
|
381
|
+
|
|
382
|
+
```
|
|
383
|
+
Content-Security-Policy: script-src 'nonce-abc1234'
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**!! Take note that this nonce need to be random on every request, as else this security is useless !!**
|
|
387
|
+
|
|
369
388
|
## Utilities
|
|
370
389
|
|
|
371
390
|
This package provides a number of utility functions.
|
package/package.json
CHANGED
|
@@ -10,8 +10,28 @@ class DataLayer {
|
|
|
10
10
|
this.userInformation = {}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* @param {Object} parameters The parameters object.
|
|
15
|
+
* @param {string} [parameters.gtmId] The GTM ID - Defaults to our default GTM tag
|
|
16
|
+
* @param {string} [parameters.nonce] The nonce value for the script tag (Used for CSP).
|
|
17
|
+
*/
|
|
18
|
+
initialize(parameters) {
|
|
19
|
+
// Backwards compatability
|
|
20
|
+
if (typeof parameters === 'string') {
|
|
21
|
+
parameters = { gtmId: parameters }
|
|
22
|
+
|
|
23
|
+
// Deprecation warning in development
|
|
24
|
+
if (process.env.NODE_ENV === 'development') {
|
|
25
|
+
console.warn(
|
|
26
|
+
'@dpgmedia/creative',
|
|
27
|
+
'DEPRECATION WARNING: new Datalayer(gtmId) is deprecated, please use new Datalayer({ gtmId }) instead.'
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { gtmId = 'GTM-TW99VZN', nonce } = parameters || {}
|
|
33
|
+
|
|
34
|
+
loadScript(`https://www.googletagmanager.com/gtm.js?id=${gtmId}`, { nonce })
|
|
15
35
|
|
|
16
36
|
this.pushGtmStart()
|
|
17
37
|
this.pushUserWhenAuthenticated()
|
package/src/config/config.js
CHANGED
|
@@ -18,10 +18,10 @@ class Configuration {
|
|
|
18
18
|
/**
|
|
19
19
|
* Retrieve the configuration for the current hostname or present query parameter and the given app (optional).
|
|
20
20
|
*/
|
|
21
|
-
async retrieveConfigForDetectedStation(appId = null) {
|
|
21
|
+
async retrieveConfigForDetectedStation(appId = null, { parameterName = 'stationId' } = {}) {
|
|
22
22
|
const parameters = new URLSearchParams(window.location.search)
|
|
23
|
-
if (parameters.has(
|
|
24
|
-
return this.retrieveConfigForStation(parameters.get(
|
|
23
|
+
if (parameters.has(parameterName)) {
|
|
24
|
+
return this.retrieveConfigForStation(parameters.get(parameterName), appId)
|
|
25
25
|
}
|
|
26
26
|
return this.retrieveConfigByHostname(appId)
|
|
27
27
|
}
|
package/src/privacy/privacy.js
CHANGED
|
@@ -15,11 +15,14 @@ class Privacy {
|
|
|
15
15
|
* @param {string} privacyManagerId e.g. '148844'
|
|
16
16
|
* @param {string} websiteUrl e.g. 'https://qmusic.nl'
|
|
17
17
|
* @param {string} cmpCname e.g. 'https://cmp.qmusic.nl'
|
|
18
|
+
* @param {Object} options Additional options.
|
|
19
|
+
* @param {string} [options.nonce] The nonce value for the script tag (Used for CSP).
|
|
18
20
|
*/
|
|
19
21
|
initialize(
|
|
20
22
|
privacyManagerId = config('privacy_manager_id'),
|
|
21
23
|
websiteUrl = config('website_url'),
|
|
22
|
-
cmpCname = config('cmp_cname')
|
|
24
|
+
cmpCname = config('cmp_cname'),
|
|
25
|
+
{ nonce = undefined } = {}
|
|
23
26
|
) {
|
|
24
27
|
window.cmpProperties = {
|
|
25
28
|
privacyManagerId,
|
|
@@ -28,7 +31,7 @@ class Privacy {
|
|
|
28
31
|
language: 'nl',
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
loadScript('https://myprivacy-static.dpgmedia.net/consent.js')
|
|
34
|
+
loadScript('https://myprivacy-static.dpgmedia.net/consent.js', { nonce })
|
|
32
35
|
|
|
33
36
|
this.requestConsentInformation()
|
|
34
37
|
}
|
|
@@ -49,10 +52,39 @@ class Privacy {
|
|
|
49
52
|
this.consentSubscribers.forEach((subscriber) => subscriber(null))
|
|
50
53
|
}, timeoutTime)
|
|
51
54
|
|
|
52
|
-
this.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
this.pushConsentGiven((consentData) => {
|
|
56
|
+
let updatedConsentData = {
|
|
57
|
+
consentString: consentData.tcString,
|
|
58
|
+
purposes: new Set(consentData.dpgConsentString.split('|')),
|
|
59
|
+
}
|
|
60
|
+
const iabPurposePromises = []
|
|
61
|
+
|
|
62
|
+
// We have 10 IAB purposes, so we create 10 promises that resolve with the purpose number or null if the purpose is not given.
|
|
63
|
+
for (let i = 1; i < 11; i++) {
|
|
64
|
+
iabPurposePromises.push(
|
|
65
|
+
new Promise((resolve) => {
|
|
66
|
+
this.push(
|
|
67
|
+
i,
|
|
68
|
+
() => resolve(i), // Resolve with the purpose number
|
|
69
|
+
() => resolve(null) // Resolve without a value, indicating that the purpose is not given
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Promise.all(iabPurposePromises).then((purposeConsents) => {
|
|
76
|
+
updatedConsentData = purposeConsents.reduce((acc, purpose) => {
|
|
77
|
+
if (purpose) {
|
|
78
|
+
acc.purposes.add(purpose)
|
|
79
|
+
}
|
|
80
|
+
return acc
|
|
81
|
+
}, updatedConsentData)
|
|
82
|
+
|
|
83
|
+
this.consent = new Consent(updatedConsentData)
|
|
84
|
+
this.consentSubscribers.forEach((subscriber) => subscriber(this.consent))
|
|
85
|
+
|
|
86
|
+
clearTimeout(timeout)
|
|
87
|
+
})
|
|
56
88
|
})
|
|
57
89
|
}
|
|
58
90
|
|
|
@@ -75,8 +107,12 @@ class Privacy {
|
|
|
75
107
|
})
|
|
76
108
|
}
|
|
77
109
|
|
|
78
|
-
push(type,
|
|
79
|
-
window._privacy.push([type,
|
|
110
|
+
push(type, successCallback, failCallback = () => {}) {
|
|
111
|
+
window._privacy.push([type, successCallback, failCallback])
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
pushConsentGiven(callback) {
|
|
115
|
+
this.push('consentgiven', callback)
|
|
80
116
|
}
|
|
81
117
|
|
|
82
118
|
pushFunctional(callback) {
|
|
@@ -84,16 +120,14 @@ class Privacy {
|
|
|
84
120
|
}
|
|
85
121
|
}
|
|
86
122
|
|
|
87
|
-
const targetedAdvertisingPurposes = [3, 4]
|
|
88
|
-
|
|
89
123
|
class Consent {
|
|
90
|
-
constructor(
|
|
91
|
-
this.consentString = consentString
|
|
92
|
-
this.purposes = purposes
|
|
124
|
+
constructor(consentData) {
|
|
125
|
+
this.consentString = consentData.consentString
|
|
126
|
+
this.purposes = consentData.purposes
|
|
93
127
|
}
|
|
94
128
|
|
|
95
129
|
allowsTargetedAdvertising() {
|
|
96
|
-
return
|
|
130
|
+
return this.purposes.has('targeted_advertising')
|
|
97
131
|
}
|
|
98
132
|
}
|
|
99
133
|
|
package/src/utils/loadScript.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
// From: https://github.com/medialaan/radio-radioplayer-frontend/blob/develop/src/general/utils/loadScript.js
|
|
2
2
|
// TODO: Refactor
|
|
3
3
|
|
|
4
|
-
export default function loadScript(url, { timeout = undefined } = {}) {
|
|
4
|
+
export default function loadScript(url, { timeout = undefined, nonce = undefined } = {}) {
|
|
5
5
|
return new Promise((resolve, reject) => {
|
|
6
6
|
let script = document.createElement('script')
|
|
7
7
|
const firstScript = document.getElementsByTagName('script')[0]
|
|
8
8
|
script.async = true
|
|
9
9
|
script.defer = true
|
|
10
|
+
if (nonce) {
|
|
11
|
+
script.nonce = nonce
|
|
12
|
+
}
|
|
10
13
|
|
|
11
14
|
const loadScriptTimeout = timeout ? setTimeout(() => reject(`Loading script [${url}] blocked.`), timeout) : null
|
|
12
15
|
|