@dpgradio/creative 7.0.4 → 7.0.6

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,17 +200,122 @@ 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)
267
+ ```
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
+
301
+ ## Authentication
302
+
303
+ ```js
304
+ import { authentication } from '@dpgradio/creative'
305
+
306
+ // Call once, at least before requiring authentication somewhere
307
+ authentication.initialize()
308
+
309
+ // Main usage: prompts for login if needed and sets the token in the API client
310
+ await authentication.require()
311
+
312
+
313
+ // Potentially helpful methods (but not required for typical usage)
314
+ authentication.isLoggedIn()
315
+ authentication.onRadioTokenChange(callback)
316
+ authentication.onLogin(callback)
317
+ authentication.askForLogin() // Does the same as require(), but does not await the result
318
+ authentication.radioToken
202
319
  ```
203
320
 
204
321
  ## Sharing Generator
@@ -244,9 +361,10 @@ This package provides a number of utility functions.
244
361
  | Function | Description |
245
362
  | --------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
246
363
  | `loadScript(url, { timeout })` | Dynammically loads a JS script. |
247
- | `openLink(url)` | App-compatible link opener. |
364
+ | `openExternalUrl(url)` | Opens external URL as separate from the current page as possible on web/app. |
248
365
  | `tap(value, callback)` | Invokes `callback` with the `value` and then returns `value`. |
249
366
  | `cdnImageUrl(endpoint[, size])` | Get a full image URL for an endpoint in a given size (`w480`, `w800`, `w1200`, `w2400`). |
250
367
  | `cdnUrl(endpoint)` | Get a full CDN URL for a given endpoint. |
251
368
  | `removePhoneNumberCountryPrefix(phoneNumber, [, prefix])` | Removes a country prefix from a phone number based on the station config `country_code`. |
252
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.4",
3
+ "version": "7.0.6",
4
4
  "description": "Support package for standalone Javascript applications",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -4,6 +4,14 @@ import { onLocalStorageChange } from '../utils/onLocalStorageChange.js'
4
4
 
5
5
  export const RADIO_TOKEN_LOCAL_STORAGE_KEY = 'radio-auth-token'
6
6
 
7
+ const inDevelopment = () => {
8
+ try {
9
+ return import.meta.env?.DEV
10
+ } catch (error) {
11
+ return false
12
+ }
13
+ }
14
+
7
15
  class Authentication {
8
16
  constructor() {
9
17
  this.radioToken = null
@@ -19,7 +27,14 @@ class Authentication {
19
27
  this.setToken(radioToken)
20
28
  }
21
29
  })
22
- hybrid.on('authenticated', ({ radioToken }) => this.setToken(radioToken))
30
+ hybrid.on('authenticated', ({ radioToken }) => {
31
+ this.setToken(radioToken)
32
+ })
33
+ // When a user logs in or out in another view of the app than the one we're currently in,
34
+ // we need to update the token once the user returns to this view.
35
+ hybrid.on('didAppear', ({ radioToken }) => {
36
+ this.setToken(radioToken ?? null)
37
+ })
23
38
  onLocalStorageChange(RADIO_TOKEN_LOCAL_STORAGE_KEY, (token) => this.setToken(token), true)
24
39
  }
25
40
 
@@ -43,7 +58,7 @@ class Authentication {
43
58
  askForLogin() {
44
59
  this.markAskingForLogin(true)
45
60
 
46
- if (import.meta?.env?.DEV) {
61
+ if (inDevelopment()) {
47
62
  this.setToken(prompt('[DEVELOPMENT] Please enter your radio token:'))
48
63
  } else if (hybrid.isNativeApp()) {
49
64
  hybrid.call('showAuthentication', { tier: 'light' })
@@ -70,7 +85,11 @@ class Authentication {
70
85
  return new Promise((resolve, reject) => {
71
86
  const onCompletion = () => (this.radioToken ? resolve() : reject('There is no authenticated user.'))
72
87
  this.onRadioTokenChange(onCompletion)
73
- this.askingForLoginListeners.push(onCompletion)
88
+ this.askingForLoginListeners.push((askingForLogin) => {
89
+ if (!askingForLogin) {
90
+ onCompletion()
91
+ }
92
+ })
74
93
 
75
94
  this.askForLogin()
76
95
  })
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
@@ -13,6 +13,7 @@ export { default as authentication } from './app/authentication.js'
13
13
 
14
14
  export { default as loadScript } from './utils/loadScript.js'
15
15
  export { default as openLink } from './utils/openLink.js'
16
+ export { default as openExternalUrl } from './utils/openExternalUrl.js'
16
17
  export { default as tap } from './utils/tap.js'
17
18
  export { cdnImageUrl, cdnUrl } from './utils/cdnUrl.js'
18
19
  export { removePhoneNumberCountryPrefix } from './utils/phoneNumber.js'
@@ -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,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