@dpgradio/creative 7.0.5 → 7.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 +118 -8
- package/package.json +1 -1
- package/src/{privacy → analytics}/dataLayer.js +2 -1
- package/src/analytics/mixpanel.js +37 -0
- package/src/app/hybrid.js +64 -6
- package/src/index.js +8 -1
- package/src/utils/decodeRadioToken.js +5 -0
- package/src/utils/openExternalUrl.js +20 -0
- package/src/utils/openLink.js +3 -0
- package/package-lock.json +0 -3891
package/README.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @dpgradio/creative
|
|
2
2
|
|
|
3
|
+
Table of contents:
|
|
4
|
+
* [Installation](#installation)
|
|
5
|
+
* [CSS: Fonts and Colors](#css-fonts-and-colors)
|
|
6
|
+
* [Config](#config)
|
|
7
|
+
* [Privacy and Tracking](#privacy-and-tracking)
|
|
8
|
+
* [API](#api)
|
|
9
|
+
* [Socket](#socket)
|
|
10
|
+
* [Hybrid](#hybrid)
|
|
11
|
+
* [Authentication](#authentication)
|
|
12
|
+
* [Sharing Generator](#sharing-generator)
|
|
13
|
+
* [Utilities](#utilities)
|
|
14
|
+
|
|
3
15
|
## Installation
|
|
4
16
|
|
|
5
17
|
```bash
|
|
@@ -140,6 +152,18 @@ dataLayer.pushVirtualPageView(brand) // brand is not required when the config is
|
|
|
140
152
|
dataLayer.pushEvent(event, data)
|
|
141
153
|
```
|
|
142
154
|
|
|
155
|
+
With Mixpanel
|
|
156
|
+
```js
|
|
157
|
+
import { mixpanel } from '@dpgradio/creative'
|
|
158
|
+
import Mixpanel from 'mixpanel-browser'
|
|
159
|
+
|
|
160
|
+
mixpanel.initialize(Mixpanel, {
|
|
161
|
+
mixpanelId: 'MIXPANEL ID',
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
mixpanel.trackEvent(event, data)
|
|
165
|
+
```
|
|
166
|
+
|
|
143
167
|
## API
|
|
144
168
|
|
|
145
169
|
With an initialized [configuration](#config):
|
|
@@ -188,18 +212,103 @@ socket.join({ station: stationId, entity: 'plays', action: 'play', options: { ba
|
|
|
188
212
|
|
|
189
213
|
## Hybrid
|
|
190
214
|
|
|
215
|
+
The app interaction layer for use in webviews in the app.
|
|
216
|
+
Interacts trough a "JavaScript bridge".
|
|
217
|
+
On iOS it uses [message handlers](https://developer.apple.com/documentation/webkit/wkscriptmessagehandler) and on Android it uses [JavascriptInterface](https://developer.android.com/guide/webapps/webview#BindingJavaScript).
|
|
218
|
+
|
|
191
219
|
```js
|
|
192
220
|
import { hyrid } from '@dpgradio/creative'
|
|
221
|
+
```
|
|
193
222
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
hybrid.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
223
|
+
### Information
|
|
224
|
+
|
|
225
|
+
#### `hybrid.appInfo()`
|
|
226
|
+
|
|
227
|
+
Gives information about the brand, platform, version etc. of the app.
|
|
228
|
+
|
|
229
|
+
#### `hybrid.isNativeApp()`
|
|
230
|
+
|
|
231
|
+
Returns `true` if the client is a native app.
|
|
232
|
+
|
|
233
|
+
#### `hybrid.isVersion({ iOS, Android })`
|
|
234
|
+
|
|
235
|
+
Returns `true` if the client is a native app and the version is equal to or lower than the given version for the given platform.
|
|
236
|
+
|
|
237
|
+
### Listen for events
|
|
238
|
+
|
|
239
|
+
The app will emit events that can be listened to.
|
|
240
|
+
For certain events, such as `appLoad` it is important to start listening as soon as possible.
|
|
241
|
+
Because this is sometimes difficult to do, the `appLoaded` method can be used to wait for the `appLoad` event.
|
|
242
|
+
And if the event has already been emitted before the method is called, it will return immediately.
|
|
243
|
+
|
|
244
|
+
The following events are available:
|
|
245
|
+
* `appLoad`: The app has loaded. If the user is logged in, it provides their radio token.
|
|
246
|
+
* `authenticated`: The user has authenticated. It provides the user's radio token.
|
|
247
|
+
* `didAppear`: The webview is visible. If the user is logged in, it provides their radio token. ⚠️ Test when exactly this event is emitted.
|
|
248
|
+
* `didHide`: The webview is hidden. ⚠️ Test when exactly this event is emitted.
|
|
249
|
+
|
|
250
|
+
#### `hybrid.on(event, callback, once)`
|
|
251
|
+
|
|
252
|
+
Listen for an event. If `once` is `true`, the callback will only be called once.
|
|
253
|
+
|
|
254
|
+
#### `hybrid.one(event, callback)`
|
|
255
|
+
|
|
256
|
+
Listen for an event. The callback will only be called once.
|
|
257
|
+
|
|
258
|
+
#### `hybrid.appLoaded()`
|
|
259
|
+
|
|
260
|
+
This method returns a promise that resolves when the `appLoad` event is emitted.
|
|
261
|
+
If the event has already been emitted before the method is called, it will return immediately.
|
|
262
|
+
|
|
263
|
+
If the user is logged in, the user's radio token is returned.
|
|
264
|
+
|
|
265
|
+
Example usage:
|
|
266
|
+
```
|
|
200
267
|
const radioToken = await hybrid.appLoaded()
|
|
201
|
-
|
|
268
|
+
await api.members.me()
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Actions
|
|
272
|
+
|
|
273
|
+
To initiate actions on the app side, we provide the methods below.
|
|
274
|
+
|
|
275
|
+
Under the hood these actions are called using the following method, which can also be used directly in case you need to call an action that is not available as a method:
|
|
276
|
+
|
|
202
277
|
```
|
|
278
|
+
hybrid.call(method, options)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
You may encounter the method `"navigateTo"` in older code.
|
|
282
|
+
This method is now deprecated and replaced by `hybrid.openUrl` and `hybrid.openPermalink`.
|
|
283
|
+
|
|
284
|
+
#### `hybrid.openUrl(url, { mode })`
|
|
285
|
+
|
|
286
|
+
Opens an external URL in a mode of choice.
|
|
287
|
+
This is not meant to open URLs on our own domains, e.g. `https://qmusic.be/this-is-a-cool-article`.
|
|
288
|
+
For that, use `hybrid.openPermalink`.
|
|
289
|
+
|
|
290
|
+
Supported modes:
|
|
291
|
+
* `seque`: Opens the URL in a windows that slides from the right, pushing onto the current page.
|
|
292
|
+
* `overlay`: Opens the URL in a full-screen modal that slides up from the bottom.
|
|
293
|
+
* `in-app-browser`: Opens the URL in an in-app browser with navigation controls. No hybrid functionality supported.
|
|
294
|
+
* `external-browser`: Opens the URL in the default browser. No hybrid functionality supported.
|
|
295
|
+
|
|
296
|
+
#### `hybrid.openPermalink(permalink)`
|
|
297
|
+
|
|
298
|
+
Opens a permalink of e.g. an article.
|
|
299
|
+
The article will be shown in a regular `seque` style.
|
|
300
|
+
|
|
301
|
+
⚠️ As of October 2023, not yet supported by the apps.
|
|
302
|
+
|
|
303
|
+
#### `hybrid.showAuthentication()`
|
|
304
|
+
|
|
305
|
+
Shows an authentication dialog.
|
|
306
|
+
|
|
307
|
+
On Android this is shown regardless of the login status, on iOS it's only shown when the user is not logged in.
|
|
308
|
+
|
|
309
|
+
#### `hybrid.changeHeight(height, { animated })`
|
|
310
|
+
|
|
311
|
+
Changes the height of the webview.
|
|
203
312
|
|
|
204
313
|
## Authentication
|
|
205
314
|
|
|
@@ -264,9 +373,10 @@ This package provides a number of utility functions.
|
|
|
264
373
|
| Function | Description |
|
|
265
374
|
| --------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
266
375
|
| `loadScript(url, { timeout })` | Dynammically loads a JS script. |
|
|
267
|
-
| `
|
|
376
|
+
| `openExternalUrl(url)` | Opens external URL as separate from the current page as possible on web/app. |
|
|
268
377
|
| `tap(value, callback)` | Invokes `callback` with the `value` and then returns `value`. |
|
|
269
378
|
| `cdnImageUrl(endpoint[, size])` | Get a full image URL for an endpoint in a given size (`w480`, `w800`, `w1200`, `w2400`). |
|
|
270
379
|
| `cdnUrl(endpoint)` | Get a full CDN URL for a given endpoint. |
|
|
271
380
|
| `removePhoneNumberCountryPrefix(phoneNumber, [, prefix])` | Removes a country prefix from a phone number based on the station config `country_code`. |
|
|
272
381
|
| `onLocalStorageChange(key, callback)` | Calls `callback` when the value of `key` in `localStorage` changes. |
|
|
382
|
+
| `decodeRadioToken(token)` | Decodes a JWT radio token. |
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import hybrid from '../app/hybrid.js'
|
|
2
2
|
import { config } from '../config/config.js'
|
|
3
|
+
import decodeRadioToken from '../utils/decodeRadioToken.js'
|
|
3
4
|
import loadScript from '../utils/loadScript.js'
|
|
4
5
|
|
|
5
6
|
class DataLayer {
|
|
@@ -83,7 +84,7 @@ class DataLayer {
|
|
|
83
84
|
|
|
84
85
|
setUserInformation(radioToken) {
|
|
85
86
|
this.userInformation = {
|
|
86
|
-
account_id:
|
|
87
|
+
account_id: decodeRadioToken(radioToken).uid,
|
|
87
88
|
loggedIn: true,
|
|
88
89
|
}
|
|
89
90
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import privacy from '../privacy/privacy.js'
|
|
2
|
+
|
|
3
|
+
class Mixpanel {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.trackingEvents = false
|
|
6
|
+
this.trackBacklog = []
|
|
7
|
+
this.mixpanel = null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
initialize(Mixpanel, { mixpanelId, trackPageview = true }) {
|
|
11
|
+
this.mixpanel = Mixpanel
|
|
12
|
+
privacy.push('analytics', () => {
|
|
13
|
+
this.mixpanel.init(mixpanelId, {
|
|
14
|
+
track_pageview: trackPageview,
|
|
15
|
+
persistence: 'localStorage',
|
|
16
|
+
api_host: 'https://api-eu.mixpanel.com',
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
while (this.trackBacklog.length > 0) {
|
|
20
|
+
const { eventName, properties } = this.trackBacklog.shift()
|
|
21
|
+
this.mixpanel.track(eventName, properties)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.trackingEvents = true
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pushEvent(eventName, properties) {
|
|
29
|
+
if (this.trackingEvents) {
|
|
30
|
+
this.mixpanel.track(eventName, properties)
|
|
31
|
+
} else {
|
|
32
|
+
this.trackBacklog.push({ eventName, properties })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default new Mixpanel()
|
package/src/app/hybrid.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import
|
|
1
|
+
import decodeRadioToken from '../utils/decodeRadioToken.js'
|
|
2
2
|
|
|
3
3
|
// Modern versions of the radio apps set up specific User-Agents:
|
|
4
4
|
// Android User-Agent: Qmusic/7.6.1 (nl.qmusic.app; build:21726; Android 11; Sdk:30; Manufacturer:OnePlus; Model: IN2013) OkHttp/ 4.9.1
|
|
5
5
|
// iOS User-Agent: Joe/265 (be.vmma.joe.app; build:1; iOS 14.8.1) Alamofire/265
|
|
6
|
-
const
|
|
6
|
+
const ANDROID_APP_REGEX =
|
|
7
7
|
/^(?<brand>.+)\/(?<storeVersion>[0-9.]+) \((?<buildName>.+); build:(?<buildVersion>\d+); (?<platform>Android) (?<osVersion>\d+); Sdk:(?<sdkVersion>\d+); Manufacturer:(?<manufacturer>.+); Model: (?<model>.+)\)/
|
|
8
|
-
const
|
|
8
|
+
const IOS_APP_REGEX =
|
|
9
9
|
/^(?<brand>.+)\/(?<buildVersion>[0-9.]+) \((?<buildName>.+); build:(?<internalBuildVersion>\d+); (?<platform>iOS) (?<osVersion>\d+\.\d+\.\d+)\)/
|
|
10
10
|
|
|
11
11
|
class Hybrid {
|
|
12
|
+
OPEN_URL_MODES = {
|
|
13
|
+
SEQUE: 'seque',
|
|
14
|
+
OVERLAY: 'overlay',
|
|
15
|
+
IN_APP_BROWSER: 'in-app-browser',
|
|
16
|
+
EXTERNAL_BROWSER: 'external-browser',
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
constructor() {
|
|
13
20
|
// Hook this on window so it can be required in multiple packs
|
|
14
21
|
window._hybridEventSubscriptions = window._hybridEventSubscriptions || {}
|
|
@@ -92,8 +99,8 @@ class Hybrid {
|
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
detectApp(userAgent) {
|
|
95
|
-
for (const
|
|
96
|
-
const match = userAgent.match(
|
|
102
|
+
for (const regex of [ANDROID_APP_REGEX, IOS_APP_REGEX]) {
|
|
103
|
+
const match = userAgent.match(regex)
|
|
97
104
|
if (match) {
|
|
98
105
|
return match.groups
|
|
99
106
|
}
|
|
@@ -101,8 +108,59 @@ class Hybrid {
|
|
|
101
108
|
return { platform: 'browser' }
|
|
102
109
|
}
|
|
103
110
|
|
|
111
|
+
/**
|
|
112
|
+
* @deprecated Use the {@link decodeRadioToken} helper instead.
|
|
113
|
+
*/
|
|
104
114
|
decodeRadioToken(token) {
|
|
105
|
-
return
|
|
115
|
+
return decodeRadioToken(token)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Opens an external URL.
|
|
120
|
+
*
|
|
121
|
+
* Supported modes:
|
|
122
|
+
* - `seque`: Opens the URL in a windows that slides from the right, pushing onto the current page.
|
|
123
|
+
* - `overlay`: Opens the URL in a full-screen modal that slides up from the bottom.
|
|
124
|
+
* - `in-app-browser`: Opens the URL in an in-app browser with navigation controls. No hybrid functionality supported.
|
|
125
|
+
* - `external-browser`: Opens the URL in the default browser. No hybrid functionality supported.
|
|
126
|
+
*
|
|
127
|
+
* This replaces the `navigateTo` method which is now deprecated.
|
|
128
|
+
*/
|
|
129
|
+
openUrl(url, { mode = 'overlay' } = {}) {
|
|
130
|
+
if (!Object.values(this.OPEN_URL_MODES).includes(mode)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Invalid openUrl mode: "${mode}", supported modes are "${Object.values(this.OPEN_URL_MODES).join('", "')}".`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.call('openUrl', { url, mode })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Opens a permalink of e.g. an article. The article will be shown in a regular `seque` style.
|
|
141
|
+
*
|
|
142
|
+
* As of October 2023, not yet supported by the apps.
|
|
143
|
+
*/
|
|
144
|
+
openPermalink(permalink) {
|
|
145
|
+
this.call('openPermalink', { permalink })
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Shows an authentication dialog.
|
|
150
|
+
*
|
|
151
|
+
* On Android this is shown regardless of the login status, on iOS it's only shown when the user is not logged in.
|
|
152
|
+
*/
|
|
153
|
+
showAuthentication() {
|
|
154
|
+
this.call('showAuthentication', { tier: 'light' })
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Changes the height of the webview.
|
|
159
|
+
*
|
|
160
|
+
* Animation is only supported on Android.
|
|
161
|
+
*/
|
|
162
|
+
changeHeight(height, { animated = false } = {}) {
|
|
163
|
+
this.call('changeHeight', { height, animated })
|
|
106
164
|
}
|
|
107
165
|
}
|
|
108
166
|
|
package/src/index.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
export { default as hybrid } from './app/hybrid.js'
|
|
2
2
|
|
|
3
|
+
// Privacy
|
|
3
4
|
export { default as privacy } from './privacy/privacy.js'
|
|
4
|
-
export { default as dataLayer } from './privacy/dataLayer.js'
|
|
5
5
|
|
|
6
|
+
// Analytics
|
|
7
|
+
export { default as dataLayer } from './analytics/dataLayer.js'
|
|
8
|
+
export { default as mixpanel } from './analytics/mixpanel.js'
|
|
9
|
+
|
|
10
|
+
// Websocket
|
|
6
11
|
export { default as socket } from './socket/socket.js'
|
|
7
12
|
|
|
8
13
|
export { default as configuration, config } from './config/config.js'
|
|
@@ -11,8 +16,10 @@ export { default as api } from './api/api.js'
|
|
|
11
16
|
|
|
12
17
|
export { default as authentication } from './app/authentication.js'
|
|
13
18
|
|
|
19
|
+
// Utils
|
|
14
20
|
export { default as loadScript } from './utils/loadScript.js'
|
|
15
21
|
export { default as openLink } from './utils/openLink.js'
|
|
22
|
+
export { default as openExternalUrl } from './utils/openExternalUrl.js'
|
|
16
23
|
export { default as tap } from './utils/tap.js'
|
|
17
24
|
export { cdnImageUrl, cdnUrl } from './utils/cdnUrl.js'
|
|
18
25
|
export { removePhoneNumberCountryPrefix } from './utils/phoneNumber.js'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import hybrid from '../app/hybrid.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Opens an external URL as separate as possible from the current page.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} url The absolute URL to open.
|
|
7
|
+
* @param {object} options Options for opening, includes the app mode see {@link hybrid.OPEN_URL_MODES}.
|
|
8
|
+
*/
|
|
9
|
+
export default function openExternalUrl(url, { appMode = hybrid.OPEN_URL_MODES.IN_APP_BROWSER } = {}) {
|
|
10
|
+
const chromeAgent = navigator.userAgent.indexOf('Chrome') > -1
|
|
11
|
+
const safariAgent = chromeAgent ? false : navigator.userAgent.indexOf('Safari') > -1
|
|
12
|
+
|
|
13
|
+
if (hybrid.isNativeApp()) {
|
|
14
|
+
hybrid.openUrl(url, { mode: appMode })
|
|
15
|
+
} else if (!safariAgent) {
|
|
16
|
+
window.open(url)
|
|
17
|
+
} else {
|
|
18
|
+
window.location = url
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/utils/openLink.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import hybrid from '../app/hybrid.js'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated Use {@link openExternalUrl} instead.
|
|
5
|
+
*/
|
|
3
6
|
export default function openLink(url) {
|
|
4
7
|
const chromeAgent = navigator.userAgent.indexOf('Chrome') > -1
|
|
5
8
|
const safariAgent = chromeAgent ? false : navigator.userAgent.indexOf('Safari') > -1
|