@dpgradio/creative 7.6.0-beta.2 → 8.1.0
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 +35 -4
- package/package-lock.json +8362 -0
- package/package.json +2 -2
- package/src/analytics/dataLayer.js +26 -2
- package/src/analytics/mixpanel.js +6 -4
- package/src/api/request.js +21 -0
- package/src/app/authentication.js +40 -5
- package/src/app/hybrid.js +4 -0
- package/src/config/config.js +3 -3
- package/src/privacy/privacy.js +52 -14
- package/src/share/ShareResult.js +4 -4
- package/src/share/Shareable.js +15 -17
- package/src/utils/loadScript.js +4 -1
- package/styles/colors/qmusic.css +8 -12
- package/styles/fonts/qmusic.css +7 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dpgradio/creative",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.1.0",
|
|
4
4
|
"description": "Support package for standalone Javascript applications",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "https://github.com/dpgradio/creative.git"
|
|
13
|
+
"url": "git+https://github.com/dpgradio/creative.git"
|
|
14
14
|
},
|
|
15
15
|
"keywords": [
|
|
16
16
|
"qmusic"
|
|
@@ -5,13 +5,37 @@ import loadScript from '../utils/loadScript.js'
|
|
|
5
5
|
|
|
6
6
|
class DataLayer {
|
|
7
7
|
constructor() {
|
|
8
|
+
if (typeof window === 'undefined') {
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
|
|
8
12
|
window.dataLayer = window.dataLayer || []
|
|
9
13
|
this.campaignDetails = {}
|
|
10
14
|
this.userInformation = {}
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
/**
|
|
18
|
+
* @param {Object} parameters The parameters object.
|
|
19
|
+
* @param {string} [parameters.gtmId] The GTM ID - Defaults to our default GTM tag
|
|
20
|
+
* @param {string} [parameters.nonce] The nonce value for the script tag (Used for CSP).
|
|
21
|
+
*/
|
|
22
|
+
initialize(parameters) {
|
|
23
|
+
// Backwards compatability
|
|
24
|
+
if (typeof parameters === 'string') {
|
|
25
|
+
parameters = { gtmId: parameters }
|
|
26
|
+
|
|
27
|
+
// Deprecation warning in development
|
|
28
|
+
if (process.env.NODE_ENV === 'development') {
|
|
29
|
+
console.warn(
|
|
30
|
+
'@dpgmedia/creative',
|
|
31
|
+
'DEPRECATION WARNING: new Datalayer(gtmId) is deprecated, please use new Datalayer({ gtmId }) instead.'
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { gtmId = 'GTM-TW99VZN', nonce } = parameters || {}
|
|
37
|
+
|
|
38
|
+
loadScript(`https://www.googletagmanager.com/gtm.js?id=${gtmId}`, { nonce })
|
|
15
39
|
|
|
16
40
|
this.pushGtmStart()
|
|
17
41
|
this.pushUserWhenAuthenticated()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import privacy from '../privacy/privacy'
|
|
1
|
+
import privacy from '../privacy/privacy.js'
|
|
2
2
|
|
|
3
3
|
class Mixpanel {
|
|
4
4
|
constructor() {
|
|
@@ -7,11 +7,11 @@ class Mixpanel {
|
|
|
7
7
|
this.mixpanel = null
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
initialize(Mixpanel, { mixpanelId,
|
|
10
|
+
initialize(Mixpanel, { mixpanelId, trackPageview = true }) {
|
|
11
11
|
this.mixpanel = Mixpanel
|
|
12
12
|
privacy.push('analytics', () => {
|
|
13
13
|
this.mixpanel.init(mixpanelId, {
|
|
14
|
-
track_pageview,
|
|
14
|
+
track_pageview: trackPageview,
|
|
15
15
|
persistence: 'localStorage',
|
|
16
16
|
api_host: 'https://api-eu.mixpanel.com',
|
|
17
17
|
})
|
|
@@ -20,10 +20,12 @@ class Mixpanel {
|
|
|
20
20
|
const { eventName, properties } = this.trackBacklog.shift()
|
|
21
21
|
this.mixpanel.track(eventName, properties)
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
this.trackingEvents = true
|
|
23
25
|
})
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
pushEvent(eventName, properties) {
|
|
27
29
|
if (this.trackingEvents) {
|
|
28
30
|
this.mixpanel.track(eventName, properties)
|
|
29
31
|
} else {
|
package/src/api/request.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import tap from '../utils/tap.js'
|
|
2
|
+
import authentication from '../app/authentication.js'
|
|
2
3
|
|
|
4
|
+
let authenticationRetries = 0
|
|
5
|
+
let tokenRefreshPromise = null
|
|
3
6
|
export default class Request {
|
|
4
7
|
constructor(baseUrl, version, errorHandlers = []) {
|
|
5
8
|
this.baseUrl = baseUrl
|
|
@@ -76,6 +79,21 @@ export default class Request {
|
|
|
76
79
|
})
|
|
77
80
|
|
|
78
81
|
if (!response.ok) {
|
|
82
|
+
if (response.status === 401 && authenticationRetries < 2) {
|
|
83
|
+
if (!tokenRefreshPromise) {
|
|
84
|
+
tokenRefreshPromise = authentication.refreshToken()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const newToken = await tokenRefreshPromise
|
|
89
|
+
authenticationRetries++
|
|
90
|
+
this.withHeader('Authorization', `Bearer ${newToken}`)
|
|
91
|
+
return await this.fetchJson(endpoint, method)
|
|
92
|
+
} finally {
|
|
93
|
+
tokenRefreshPromise = null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
79
97
|
this.errorHandlers.forEach((handler) => handler({ response }))
|
|
80
98
|
|
|
81
99
|
throw new Error(
|
|
@@ -84,6 +102,9 @@ export default class Request {
|
|
|
84
102
|
)
|
|
85
103
|
}
|
|
86
104
|
|
|
105
|
+
// reset authentication retries
|
|
106
|
+
this.authenticationRetries = 0
|
|
107
|
+
|
|
87
108
|
try {
|
|
88
109
|
return await response.json()
|
|
89
110
|
} catch (error) {
|
|
@@ -22,11 +22,14 @@ class Authentication {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
initialize() {
|
|
25
|
-
hybrid
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
hybrid
|
|
26
|
+
.appLoaded()
|
|
27
|
+
.then((radioToken) => {
|
|
28
|
+
if (radioToken) {
|
|
29
|
+
this.setToken(radioToken)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.catch(() => {}) // We don't have to do anything if there is no radio token at app load
|
|
30
33
|
hybrid.on('authenticated', ({ radioToken }) => {
|
|
31
34
|
this.setToken(radioToken)
|
|
32
35
|
})
|
|
@@ -110,6 +113,38 @@ class Authentication {
|
|
|
110
113
|
}
|
|
111
114
|
})
|
|
112
115
|
}
|
|
116
|
+
|
|
117
|
+
async refreshToken() {
|
|
118
|
+
if (hybrid.isNativeApp()) {
|
|
119
|
+
const updatedToken = Promise.race([
|
|
120
|
+
new Promise((resolve) => {
|
|
121
|
+
hybrid.on('authenticated', (token) => resolve(token))
|
|
122
|
+
}),
|
|
123
|
+
new Promise((_, reject) => {
|
|
124
|
+
setTimeout(() => reject(new Error('Timeout: no token received')), 2000)
|
|
125
|
+
}),
|
|
126
|
+
])
|
|
127
|
+
|
|
128
|
+
hybrid.call('refreshExpiredToken')
|
|
129
|
+
|
|
130
|
+
return (await updatedToken).radioToken
|
|
131
|
+
} else {
|
|
132
|
+
try {
|
|
133
|
+
const response = await fetch('/login/refresh', {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: {
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
const token = (await response.json()).radioToken
|
|
140
|
+
localStorage.setItem(RADIO_TOKEN_LOCAL_STORAGE_KEY, token)
|
|
141
|
+
return token
|
|
142
|
+
} catch (e) {
|
|
143
|
+
localStorage.removeItem(RADIO_TOKEN_LOCAL_STORAGE_KEY)
|
|
144
|
+
throw e
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
113
148
|
}
|
|
114
149
|
|
|
115
150
|
export default new Authentication()
|
package/src/app/hybrid.js
CHANGED
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
|
@@ -3,6 +3,10 @@ import loadScript from '../utils/loadScript.js'
|
|
|
3
3
|
|
|
4
4
|
class Privacy {
|
|
5
5
|
constructor() {
|
|
6
|
+
if (typeof window === 'undefined') {
|
|
7
|
+
return
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
window._privacy = window._privacy || []
|
|
7
11
|
|
|
8
12
|
this.consent = undefined
|
|
@@ -15,11 +19,14 @@ class Privacy {
|
|
|
15
19
|
* @param {string} privacyManagerId e.g. '148844'
|
|
16
20
|
* @param {string} websiteUrl e.g. 'https://qmusic.nl'
|
|
17
21
|
* @param {string} cmpCname e.g. 'https://cmp.qmusic.nl'
|
|
22
|
+
* @param {Object} options Additional options.
|
|
23
|
+
* @param {string} [options.nonce] The nonce value for the script tag (Used for CSP).
|
|
18
24
|
*/
|
|
19
25
|
initialize(
|
|
20
26
|
privacyManagerId = config('privacy_manager_id'),
|
|
21
27
|
websiteUrl = config('website_url'),
|
|
22
|
-
cmpCname = config('cmp_cname')
|
|
28
|
+
cmpCname = config('cmp_cname'),
|
|
29
|
+
{ nonce = undefined } = {}
|
|
23
30
|
) {
|
|
24
31
|
window.cmpProperties = {
|
|
25
32
|
privacyManagerId,
|
|
@@ -28,7 +35,7 @@ class Privacy {
|
|
|
28
35
|
language: 'nl',
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
loadScript('https://myprivacy-static.dpgmedia.net/consent.js')
|
|
38
|
+
loadScript('https://myprivacy-static.dpgmedia.net/consent.js', { nonce })
|
|
32
39
|
|
|
33
40
|
this.requestConsentInformation()
|
|
34
41
|
}
|
|
@@ -49,10 +56,39 @@ class Privacy {
|
|
|
49
56
|
this.consentSubscribers.forEach((subscriber) => subscriber(null))
|
|
50
57
|
}, timeoutTime)
|
|
51
58
|
|
|
52
|
-
this.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
this.pushConsentGiven((consentData) => {
|
|
60
|
+
let updatedConsentData = {
|
|
61
|
+
consentString: consentData.tcString,
|
|
62
|
+
purposes: new Set(consentData.dpgConsentString.split('|')),
|
|
63
|
+
}
|
|
64
|
+
const iabPurposePromises = []
|
|
65
|
+
|
|
66
|
+
// We have 10 IAB purposes, so we create 10 promises that resolve with the purpose number or null if the purpose is not given.
|
|
67
|
+
for (let i = 1; i < 11; i++) {
|
|
68
|
+
iabPurposePromises.push(
|
|
69
|
+
new Promise((resolve) => {
|
|
70
|
+
this.push(
|
|
71
|
+
i,
|
|
72
|
+
() => resolve(i), // Resolve with the purpose number
|
|
73
|
+
() => resolve(null) // Resolve without a value, indicating that the purpose is not given
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Promise.all(iabPurposePromises).then((purposeConsents) => {
|
|
80
|
+
updatedConsentData = purposeConsents.reduce((acc, purpose) => {
|
|
81
|
+
if (purpose) {
|
|
82
|
+
acc.purposes.add(purpose)
|
|
83
|
+
}
|
|
84
|
+
return acc
|
|
85
|
+
}, updatedConsentData)
|
|
86
|
+
|
|
87
|
+
this.consent = new Consent(updatedConsentData)
|
|
88
|
+
this.consentSubscribers.forEach((subscriber) => subscriber(this.consent))
|
|
89
|
+
|
|
90
|
+
clearTimeout(timeout)
|
|
91
|
+
})
|
|
56
92
|
})
|
|
57
93
|
}
|
|
58
94
|
|
|
@@ -75,8 +111,12 @@ class Privacy {
|
|
|
75
111
|
})
|
|
76
112
|
}
|
|
77
113
|
|
|
78
|
-
push(type,
|
|
79
|
-
window._privacy.push([type,
|
|
114
|
+
push(type, successCallback, failCallback = () => {}) {
|
|
115
|
+
window._privacy.push([type, successCallback, failCallback])
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
pushConsentGiven(callback) {
|
|
119
|
+
this.push('consentgiven', callback)
|
|
80
120
|
}
|
|
81
121
|
|
|
82
122
|
pushFunctional(callback) {
|
|
@@ -84,16 +124,14 @@ class Privacy {
|
|
|
84
124
|
}
|
|
85
125
|
}
|
|
86
126
|
|
|
87
|
-
const targetedAdvertisingPurposes = [3, 4]
|
|
88
|
-
|
|
89
127
|
class Consent {
|
|
90
|
-
constructor(
|
|
91
|
-
this.consentString = consentString
|
|
92
|
-
this.purposes = purposes
|
|
128
|
+
constructor(consentData) {
|
|
129
|
+
this.consentString = consentData.consentString
|
|
130
|
+
this.purposes = consentData.purposes
|
|
93
131
|
}
|
|
94
132
|
|
|
95
133
|
allowsTargetedAdvertising() {
|
|
96
|
-
return
|
|
134
|
+
return this.purposes.has('targeted_advertising')
|
|
97
135
|
}
|
|
98
136
|
}
|
|
99
137
|
|
package/src/share/ShareResult.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import openExternalUrl from '../utils/openExternalUrl.js'
|
|
2
2
|
|
|
3
3
|
export default class ShareResult {
|
|
4
4
|
constructor(shareable, { url, image }) {
|
|
@@ -20,14 +20,14 @@ export default class ShareResult {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
openFacebookUrl() {
|
|
23
|
-
|
|
23
|
+
openExternalUrl(this.facebookUrl())
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
openWhatsappUrl() {
|
|
27
|
-
|
|
27
|
+
openExternalUrl(this.whatsappUrl())
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
openInstagramUrl() {
|
|
31
|
-
|
|
31
|
+
openExternalUrl(this.instagramUrl())
|
|
32
32
|
}
|
|
33
33
|
}
|
package/src/share/Shareable.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// eslint-disable-next-line import/no-unresolved -- TODO: Needs a rewrite to fetch
|
|
2
|
-
import axios from 'axios'
|
|
3
1
|
import ShareResult from './ShareResult.js'
|
|
4
2
|
|
|
5
3
|
export default class Shareable {
|
|
@@ -36,21 +34,21 @@ export default class Shareable {
|
|
|
36
34
|
return this
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
generateUsingImage(imageGeneratorProperties) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
})
|
|
52
|
-
.then((response) => resolve(new ShareResult(this, response.data)))
|
|
53
|
-
.catch(reject)
|
|
37
|
+
async generateUsingImage(imageGeneratorProperties) {
|
|
38
|
+
const response = await fetch('https://dba1du5ckc.execute-api.eu-west-3.amazonaws.com/prod/', {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({
|
|
44
|
+
title: this.title,
|
|
45
|
+
description: this.description,
|
|
46
|
+
image_generator: imageGeneratorProperties.toJson(),
|
|
47
|
+
redirect_url: this.redirectUrl,
|
|
48
|
+
domain: this.domain,
|
|
49
|
+
}),
|
|
54
50
|
})
|
|
51
|
+
const data = await response.json()
|
|
52
|
+
return new ShareResult(this, data)
|
|
55
53
|
}
|
|
56
54
|
}
|
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
|
|
package/styles/colors/qmusic.css
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
--q-red: 237 54 36;
|
|
3
|
-
--q-
|
|
4
|
-
--q-teal: 0 191 179;
|
|
5
|
-
--q-yellow: 249 212 35;
|
|
6
|
-
--q-purple: 210 110 230;
|
|
7
|
-
--q-green: 151 215 0;
|
|
8
|
-
--q-blue: 2 134 255;
|
|
3
|
+
--q-red-dark: 217 46 32;
|
|
9
4
|
|
|
10
|
-
--q-
|
|
11
|
-
--q-
|
|
5
|
+
--q-grey: 61 61 61;
|
|
6
|
+
--q-grey-light: 230 230 230;
|
|
7
|
+
--q-grey-dark: 43 43 43;
|
|
12
8
|
|
|
13
|
-
--q-
|
|
14
|
-
--q-
|
|
15
|
-
--q-
|
|
16
|
-
--q-
|
|
9
|
+
--q-green: 168 216 50;
|
|
10
|
+
--q-yellow: 249 243 197;
|
|
11
|
+
--q-purple: 252 104 182;
|
|
12
|
+
--q-blue: 124 186 247;
|
|
17
13
|
}
|
package/styles/fonts/qmusic.css
CHANGED
|
@@ -41,8 +41,11 @@
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
@font-face {
|
|
44
|
-
font-family:
|
|
45
|
-
src: url(https://fonts.qmusic.be/
|
|
46
|
-
|
|
47
|
-
font-style: normal
|
|
44
|
+
font-family: BlackBones;
|
|
45
|
+
src: url(https://fonts.qmusic.be/black-bones.ttf), url(https://fonts.qmusic.be/black-bones.eot), url(https://fonts.qmusic.be/black-bones.woff), url(https://fonts.qmusic.be/black-bones.woff2), url(https://fonts.qmusic.be/black-bones.svg);
|
|
46
|
+
font-weight: normal;
|
|
47
|
+
font-style: normal;
|
|
48
|
+
font-display: fallback;
|
|
48
49
|
}
|
|
50
|
+
|
|
51
|
+
|