@dpgradio/creative 7.0.5 → 7.6.0-beta.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 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
@@ -188,19 +200,104 @@ socket.join({ station: stationId, entity: 'plays', action: 'play', options: { ba
188
200
 
189
201
  ## Hybrid
190
202
 
203
+ The app interaction layer for use in webviews in the app.
204
+ Interacts trough a "JavaScript bridge".
205
+ 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).
206
+
191
207
  ```js
192
208
  import { hyrid } from '@dpgradio/creative'
209
+ ```
193
210
 
194
- hybrid.appInfo
195
- hybrid.isNativeApp()
196
- hybrid.isVersion({ iOS, Android })
197
- hybrid.call(method, options)
198
- hybrid.on(method, callback, once)
199
- hybrid.one(method, callback)
211
+ ### Information
212
+
213
+ #### `hybrid.appInfo()`
214
+
215
+ Gives information about the brand, platform, version etc. of the app.
216
+
217
+ #### `hybrid.isNativeApp()`
218
+
219
+ Returns `true` if the client is a native app.
220
+
221
+ #### `hybrid.isVersion({ iOS, Android })`
222
+
223
+ 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.
224
+
225
+ ### Listen for events
226
+
227
+ The app will emit events that can be listened to.
228
+ For certain events, such as `appLoad` it is important to start listening as soon as possible.
229
+ Because this is sometimes difficult to do, the `appLoaded` method can be used to wait for the `appLoad` event.
230
+ And if the event has already been emitted before the method is called, it will return immediately.
231
+
232
+ The following events are available:
233
+ * `appLoad`: The app has loaded. If the user is logged in, it provides their radio token.
234
+ * `authenticated`: The user has authenticated. It provides the user's radio token.
235
+ * `didAppear`: The webview is visible. If the user is logged in, it provides their radio token. ⚠️ Test when exactly this event is emitted.
236
+ * `didHide`: The webview is hidden. ⚠️ Test when exactly this event is emitted.
237
+
238
+ #### `hybrid.on(event, callback, once)`
239
+
240
+ Listen for an event. If `once` is `true`, the callback will only be called once.
241
+
242
+ #### `hybrid.one(event, callback)`
243
+
244
+ Listen for an event. The callback will only be called once.
245
+
246
+ #### `hybrid.appLoaded()`
247
+
248
+ This method returns a promise that resolves when the `appLoad` event is emitted.
249
+ If the event has already been emitted before the method is called, it will return immediately.
250
+
251
+ If the user is logged in, the user's radio token is returned.
252
+
253
+ Example usage:
254
+ ```
200
255
  const radioToken = await hybrid.appLoaded()
201
- hybrid.decodeRadioToken(radioToken)
256
+ await api.members.me()
257
+ ```
258
+
259
+ ### Actions
260
+
261
+ To initiate actions on the app side, we provide the methods below.
262
+
263
+ 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:
264
+
265
+ ```
266
+ hybrid.call(method, options)
202
267
  ```
203
268
 
269
+ You may encounter the method `"navigateTo"` in older code.
270
+ This method is now deprecated and replaced by `hybrid.openUrl` and `hybrid.openPermalink`.
271
+
272
+ #### `hybrid.openUrl(url, { mode })`
273
+
274
+ Opens an external URL in a mode of choice.
275
+ This is not meant to open URLs on our own domains, e.g. `https://qmusic.be/this-is-a-cool-article`.
276
+ For that, use `hybrid.openPermalink`.
277
+
278
+ Supported modes:
279
+ * `seque`: Opens the URL in a windows that slides from the right, pushing onto the current page.
280
+ * `overlay`: Opens the URL in a full-screen modal that slides up from the bottom.
281
+ * `in-app-browser`: Opens the URL in an in-app browser with navigation controls. No hybrid functionality supported.
282
+ * `external-browser`: Opens the URL in the default browser. No hybrid functionality supported.
283
+
284
+ #### `hybrid.openPermalink(permalink)`
285
+
286
+ Opens a permalink of e.g. an article.
287
+ The article will be shown in a regular `seque` style.
288
+
289
+ ⚠️ As of October 2023, not yet supported by the apps.
290
+
291
+ #### `hybrid.showAuthentication()`
292
+
293
+ Shows an authentication dialog.
294
+
295
+ On Android this is shown regardless of the login status, on iOS it's only shown when the user is not logged in.
296
+
297
+ #### `hybrid.changeHeight(height, { animated })`
298
+
299
+ Changes the height of the webview.
300
+
204
301
  ## Authentication
205
302
 
206
303
  ```js
@@ -264,9 +361,10 @@ This package provides a number of utility functions.
264
361
  | Function | Description |
265
362
  | --------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
266
363
  | `loadScript(url, { timeout })` | Dynammically loads a JS script. |
267
- | `openLink(url)` | App-compatible link opener. |
364
+ | `openExternalUrl(url)` | Opens external URL as separate from the current page as possible on web/app. |
268
365
  | `tap(value, callback)` | Invokes `callback` with the `value` and then returns `value`. |
269
366
  | `cdnImageUrl(endpoint[, size])` | Get a full image URL for an endpoint in a given size (`w480`, `w800`, `w1200`, `w2400`). |
270
367
  | `cdnUrl(endpoint)` | Get a full CDN URL for a given endpoint. |
271
368
  | `removePhoneNumberCountryPrefix(phoneNumber, [, prefix])` | Removes a country prefix from a phone number based on the station config `country_code`. |
272
369
  | `onLocalStorageChange(key, callback)` | Calls `callback` when the value of `key` in `localStorage` changes. |
370
+ | `decodeRadioToken(token)` | Decodes a JWT radio token. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dpgradio/creative",
3
- "version": "7.0.5",
3
+ "version": "7.6.0-beta.1",
4
4
  "description": "Support package for standalone Javascript applications",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -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: hybrid.decodeRadioToken(radioToken).uid,
87
+ account_id: decodeRadioToken(radioToken).uid,
87
88
  loggedIn: true,
88
89
  }
89
90
  }
@@ -0,0 +1,35 @@
1
+ import { privacy } from '../privacy/privacy'
2
+
3
+ class Mixpanel {
4
+ constructor() {
5
+ this.trackingEvents = false
6
+ this.trackBacklog = []
7
+ this.mixpanel = null
8
+ }
9
+
10
+ initialize(Mixpanel, { mixpanelId, track_pageview = true }) {
11
+ this.mixpanel = Mixpanel
12
+ privacy.push('analytics', () => {
13
+ this.mixpanel.init(mixpanelId, {
14
+ track_pageview,
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
+ }
25
+
26
+ trackEvent(eventName, properties) {
27
+ if (this.trackingEvents) {
28
+ this.mixpanel.track(eventName, properties)
29
+ } else {
30
+ this.trackBacklog.push({ eventName, properties })
31
+ }
32
+ }
33
+ }
34
+
35
+ export default new Mixpanel()
package/src/app/hybrid.js CHANGED
@@ -1,14 +1,21 @@
1
- import jwtDecode from 'jwt-decode'
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 androidRegexp =
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 iOSRegexp =
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 regexp of [androidRegexp, iOSRegexp]) {
96
- const match = userAgent.match(regexp)
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 jwtDecode(token)
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,5 @@
1
+ import jwtDecode from 'jwt-decode'
2
+
3
+ export default function decodeRadioToken(token) {
4
+ return jwtDecode(token)
5
+ }
@@ -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
+ }
@@ -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