@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 +126 -8
- package/package.json +1 -1
- package/src/app/authentication.js +22 -3
- package/src/app/hybrid.js +64 -6
- package/src/index.js +1 -0
- package/src/privacy/dataLayer.js +2 -1
- package/src/utils/decodeRadioToken.js +5 -0
- package/src/utils/openExternalUrl.js +20 -0
- package/src/utils/openLink.js +3 -0
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
|
-
|
|
195
|
-
|
|
196
|
-
hybrid.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
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
|
@@ -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 }) =>
|
|
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 (
|
|
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(
|
|
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
|
|
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
|
@@ -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'
|
package/src/privacy/dataLayer.js
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,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
|