@wwdrew/expo-spotify-sdk 0.8.0 → 2.0.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 +404 -140
- package/android/build.gradle +12 -2
- package/android/consumer-rules.pro +5 -0
- package/android/libs/SETUP.md +29 -0
- package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +146 -9
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteCoordinator.kt +556 -0
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyErrors.kt +112 -0
- package/app.plugin.js +1 -1
- package/build/ExpoSpotifySDKModule.web.d.ts +23 -0
- package/build/ExpoSpotifySDKModule.web.d.ts.map +1 -1
- package/build/ExpoSpotifySDKModule.web.js +74 -5
- package/build/ExpoSpotifySDKModule.web.js.map +1 -1
- package/build/app-remote/error.d.ts +8 -0
- package/build/app-remote/error.d.ts.map +1 -0
- package/build/app-remote/error.js +10 -0
- package/build/app-remote/error.js.map +1 -0
- package/build/app-remote/index.d.ts +79 -0
- package/build/app-remote/index.d.ts.map +1 -0
- package/build/app-remote/index.js +110 -0
- package/build/app-remote/index.js.map +1 -0
- package/build/auth/error.d.ts +8 -0
- package/build/auth/error.d.ts.map +1 -0
- package/build/auth/error.js +10 -0
- package/build/auth/error.js.map +1 -0
- package/build/auth/index.d.ts +124 -0
- package/build/auth/index.d.ts.map +1 -0
- package/build/auth/index.js +162 -0
- package/build/auth/index.js.map +1 -0
- package/build/content/error.d.ts +8 -0
- package/build/content/error.d.ts.map +1 -0
- package/build/content/error.js +10 -0
- package/build/content/error.js.map +1 -0
- package/build/content/index.d.ts +38 -0
- package/build/content/index.d.ts.map +1 -0
- package/build/content/index.js +56 -0
- package/build/content/index.js.map +1 -0
- package/build/error.d.ts +24 -0
- package/build/error.d.ts.map +1 -0
- package/build/error.js +25 -0
- package/build/error.js.map +1 -0
- package/build/hooks/index.d.ts +90 -0
- package/build/hooks/index.d.ts.map +1 -0
- package/build/hooks/index.js +326 -0
- package/build/hooks/index.js.map +1 -0
- package/build/images/error.d.ts +8 -0
- package/build/images/error.d.ts.map +1 -0
- package/build/images/error.js +10 -0
- package/build/images/error.js.map +1 -0
- package/build/images/index.d.ts +36 -0
- package/build/images/index.d.ts.map +1 -0
- package/build/images/index.js +59 -0
- package/build/images/index.js.map +1 -0
- package/build/index.d.ts +44 -56
- package/build/index.d.ts.map +1 -1
- package/build/index.js +49 -141
- package/build/index.js.map +1 -1
- package/build/player/error.d.ts +8 -0
- package/build/player/error.d.ts.map +1 -0
- package/build/player/error.js +10 -0
- package/build/player/error.js.map +1 -0
- package/build/player/index.d.ts +140 -0
- package/build/player/index.d.ts.map +1 -0
- package/build/player/index.js +137 -0
- package/build/player/index.js.map +1 -0
- package/build/uri/index.d.ts +55 -0
- package/build/uri/index.d.ts.map +1 -0
- package/build/uri/index.js +57 -0
- package/build/uri/index.js.map +1 -0
- package/build/user/error.d.ts +8 -0
- package/build/user/error.d.ts.map +1 -0
- package/build/user/error.js +10 -0
- package/build/user/error.js.map +1 -0
- package/build/user/index.d.ts +44 -0
- package/build/user/index.d.ts.map +1 -0
- package/build/user/index.js +97 -0
- package/build/user/index.js.map +1 -0
- package/ios/ExpoSpotifySDK.podspec +1 -1
- package/ios/ExpoSpotifySDKModule.swift +227 -5
- package/ios/SpotifyAppRemoteCoordinator.swift +939 -0
- package/ios/SpotifyAuthCoordinator.swift +53 -13
- package/ios/SpotifyTokenRefreshClient.swift +28 -0
- package/package.json +29 -16
- package/plugin/build/index.d.ts +20 -3
- package/plugin/build/index.js +22 -25
- package/plugin/build/types.d.ts +17 -1
- package/plugin/build/withSpotifySdkConfig.d.ts +4 -0
- package/plugin/build/withSpotifySdkConfig.js +27 -0
package/android/build.gradle
CHANGED
|
@@ -16,10 +16,10 @@ buildscript {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
project.android {
|
|
19
|
-
compileSdkVersion safeExtGet("compileSdkVersion",
|
|
19
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 36)
|
|
20
20
|
defaultConfig {
|
|
21
21
|
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
22
|
-
targetSdkVersion safeExtGet("targetSdkVersion",
|
|
22
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 36)
|
|
23
23
|
}
|
|
24
24
|
compileOptions {
|
|
25
25
|
sourceCompatibility JavaVersion.VERSION_17
|
|
@@ -48,6 +48,16 @@ repositories {
|
|
|
48
48
|
|
|
49
49
|
dependencies {
|
|
50
50
|
implementation "com.spotify.android:auth:4.0.1"
|
|
51
|
+
|
|
52
|
+
// Spotify App Remote SDK — NOT on Maven Central.
|
|
53
|
+
// Download spotify-app-remote-release-0.8.0.aar from
|
|
54
|
+
// https://github.com/spotify/android-sdk/releases and place it in android/libs/.
|
|
55
|
+
// See android/libs/SETUP.md for full instructions.
|
|
56
|
+
implementation files('libs/spotify-app-remote-release-0.8.0.aar')
|
|
57
|
+
// App Remote is consumed as a local AAR, so transitive deps like Gson must
|
|
58
|
+
// be declared explicitly to avoid NoClassDefFoundError at runtime.
|
|
59
|
+
implementation "com.google.code.gson:gson:2.11.0"
|
|
60
|
+
|
|
51
61
|
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
|
52
62
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1"
|
|
53
63
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
-keepclassmembers class com.spotify.sdk.android.auth.** { *; }
|
|
4
4
|
-dontwarn com.spotify.sdk.android.auth.**
|
|
5
5
|
|
|
6
|
+
# Keep Spotify App Remote SDK classes
|
|
7
|
+
-keep class com.spotify.android.appremote.** { *; }
|
|
8
|
+
-keepclassmembers class com.spotify.android.appremote.** { *; }
|
|
9
|
+
-dontwarn com.spotify.android.appremote.**
|
|
10
|
+
|
|
6
11
|
# Keep our module's data classes
|
|
7
12
|
-keep class expo.modules.spotifysdk.** { *; }
|
|
8
13
|
-keepclassmembers class expo.modules.spotifysdk.** { *; }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Android App Remote SDK setup
|
|
2
|
+
|
|
3
|
+
The Spotify App Remote SDK for Android is not published to Maven Central and must be
|
|
4
|
+
downloaded manually.
|
|
5
|
+
|
|
6
|
+
## Steps
|
|
7
|
+
|
|
8
|
+
1. Go to <https://github.com/spotify/android-sdk/releases>
|
|
9
|
+
2. Download the latest release archive (e.g. `spotify-android-sdk-x.x.x.zip`)
|
|
10
|
+
3. Extract `spotify-app-remote-release-x.x.x.aar` from the archive
|
|
11
|
+
4. Rename it to `spotify-app-remote-release-0.8.0.aar` (or update the version in
|
|
12
|
+
`android/build.gradle` to match the downloaded version)
|
|
13
|
+
5. Place the `.aar` file in this directory (`android/libs/`)
|
|
14
|
+
|
|
15
|
+
The `flatDir` repository in `android/build.gradle` will pick it up automatically.
|
|
16
|
+
|
|
17
|
+
## Why not Maven?
|
|
18
|
+
|
|
19
|
+
The Spotify Auth SDK (`com.spotify.android:auth`) is on Maven Central, but the App Remote
|
|
20
|
+
SDK is distributed exclusively via GitHub releases. This directory follows the same pattern
|
|
21
|
+
as the iOS side, where `SpotifyiOS.xcframework` is committed directly to `ios/SpotifySDK/`.
|
|
22
|
+
|
|
23
|
+
## Proguard
|
|
24
|
+
|
|
25
|
+
`consumer-rules.pro` already keeps the App Remote classes:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
-keep class com.spotify.android.appremote.** { *; }
|
|
29
|
+
```
|
|
@@ -8,13 +8,18 @@ import expo.modules.kotlin.functions.Coroutine
|
|
|
8
8
|
import expo.modules.kotlin.modules.Module
|
|
9
9
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
10
10
|
|
|
11
|
-
private const val SDK_VERSION = "0.
|
|
11
|
+
private const val SDK_VERSION = "2.0.0" // x-release-please-version
|
|
12
12
|
private const val EVENT_SESSION_CHANGE = "onSessionChange"
|
|
13
|
+
private const val EVENT_CONNECTION_STATE_CHANGE = "onConnectionStateChange"
|
|
14
|
+
private const val EVENT_CONNECTION_ERROR = "onConnectionError"
|
|
15
|
+
private const val EVENT_PLAYER_STATE_CHANGE = "onPlayerStateChange"
|
|
16
|
+
private const val EVENT_CAPABILITIES_CHANGE = "onCapabilitiesChange"
|
|
13
17
|
|
|
14
18
|
class ExpoSpotifySDKModule : Module() {
|
|
15
19
|
|
|
16
20
|
private lateinit var authLauncher: AppContextActivityResultLauncher<SpotifyAuthInput, AuthorizationResponse>
|
|
17
|
-
private val
|
|
21
|
+
private val authCoordinator = SpotifyAuthCoordinator()
|
|
22
|
+
private val appRemoteCoordinator = SpotifyAppRemoteCoordinator()
|
|
18
23
|
|
|
19
24
|
private val context
|
|
20
25
|
get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
|
|
@@ -58,17 +63,41 @@ class ExpoSpotifySDKModule : Module() {
|
|
|
58
63
|
)
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
private fun
|
|
66
|
+
private fun emitSessionError(code: String, message: String) {
|
|
62
67
|
sendEvent(
|
|
63
68
|
EVENT_SESSION_CHANGE,
|
|
64
|
-
mapOf("type" to
|
|
69
|
+
mapOf("type" to "didFail", "error" to mapOf("code" to code, "message" to message)),
|
|
65
70
|
)
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
override fun definition() = ModuleDefinition {
|
|
69
74
|
Name("ExpoSpotifySDK")
|
|
70
75
|
|
|
71
|
-
Events(
|
|
76
|
+
Events(
|
|
77
|
+
EVENT_SESSION_CHANGE,
|
|
78
|
+
EVENT_CONNECTION_STATE_CHANGE,
|
|
79
|
+
EVENT_CONNECTION_ERROR,
|
|
80
|
+
EVENT_PLAYER_STATE_CHANGE,
|
|
81
|
+
EVENT_CAPABILITIES_CHANGE,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
// Wire up App Remote coordinator event callbacks once the module is alive.
|
|
85
|
+
OnCreate {
|
|
86
|
+
appRemoteCoordinator.onConnectionStateChange = { state ->
|
|
87
|
+
sendEvent(EVENT_CONNECTION_STATE_CHANGE, mapOf("state" to state))
|
|
88
|
+
}
|
|
89
|
+
appRemoteCoordinator.onConnectionError = { code, message ->
|
|
90
|
+
sendEvent(EVENT_CONNECTION_ERROR, mapOf("code" to code, "message" to message))
|
|
91
|
+
}
|
|
92
|
+
appRemoteCoordinator.onPlayerStateChange = { stateMap ->
|
|
93
|
+
sendEvent(EVENT_PLAYER_STATE_CHANGE, stateMap)
|
|
94
|
+
}
|
|
95
|
+
appRemoteCoordinator.onCapabilitiesChange = { capabilities ->
|
|
96
|
+
sendEvent(EVENT_CAPABILITIES_CHANGE, capabilities)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Auth ────────────────────────────────────────────────────────────────
|
|
72
101
|
|
|
73
102
|
Function("isAvailable") {
|
|
74
103
|
isSpotifyInstalled()
|
|
@@ -97,7 +126,7 @@ class ExpoSpotifySDKModule : Module() {
|
|
|
97
126
|
showDialog = config.showDialog,
|
|
98
127
|
)
|
|
99
128
|
|
|
100
|
-
val response =
|
|
129
|
+
val response = authCoordinator.authenticate(authLauncher, input)
|
|
101
130
|
|
|
102
131
|
val payload = when (response.type) {
|
|
103
132
|
AuthorizationResponse.Type.TOKEN -> SpotifySessionPayload(
|
|
@@ -128,14 +157,14 @@ class ExpoSpotifySDKModule : Module() {
|
|
|
128
157
|
AuthorizationResponse.Type.ERROR -> throw SpotifyAuthErrorException(
|
|
129
158
|
response.error ?: "Spotify returned an unspecified error",
|
|
130
159
|
)
|
|
131
|
-
AuthorizationResponse.Type.UNKNOWN
|
|
160
|
+
AuthorizationResponse.Type.UNKNOWN -> throw UnknownSpotifyException(
|
|
132
161
|
"Unknown Spotify authorization response type",
|
|
133
162
|
)
|
|
134
163
|
}
|
|
135
164
|
emitSession("didInitiate", payload)
|
|
136
165
|
payload.toMap()
|
|
137
166
|
} catch (e: expo.modules.kotlin.exception.CodedException) {
|
|
138
|
-
|
|
167
|
+
emitSessionError(e.code ?: "UNKNOWN", e.localizedMessage ?: e.code ?: "Unknown error")
|
|
139
168
|
throw e
|
|
140
169
|
}
|
|
141
170
|
}
|
|
@@ -158,9 +187,117 @@ class ExpoSpotifySDKModule : Module() {
|
|
|
158
187
|
emitSession("didRenew", payload)
|
|
159
188
|
payload.toMap()
|
|
160
189
|
} catch (e: expo.modules.kotlin.exception.CodedException) {
|
|
161
|
-
|
|
190
|
+
emitSessionError(e.code ?: "UNKNOWN", e.localizedMessage ?: e.code ?: "Unknown error")
|
|
162
191
|
throw e
|
|
163
192
|
}
|
|
164
193
|
}
|
|
194
|
+
|
|
195
|
+
// ── App Remote ──────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
AsyncFunction("appRemoteConnect") Coroutine { accessToken: String ->
|
|
198
|
+
val manifest = readManifestConfig()
|
|
199
|
+
appRemoteCoordinator.connect(
|
|
200
|
+
context = context,
|
|
201
|
+
clientId = manifest.clientId,
|
|
202
|
+
redirectUri = manifest.redirectUri,
|
|
203
|
+
accessToken = accessToken,
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
AsyncFunction("appRemoteDisconnect") Coroutine { ->
|
|
208
|
+
appRemoteCoordinator.disconnect()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
Function("appRemoteIsConnected") {
|
|
212
|
+
appRemoteCoordinator.isConnected()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
AsyncFunction("appRemoteGetConnectionState") Coroutine { ->
|
|
216
|
+
appRemoteCoordinator.getConnectionState()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Player ──────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
AsyncFunction("playerPlay") Coroutine { uri: String ->
|
|
222
|
+
appRemoteCoordinator.playerPlay(uri)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
AsyncFunction("playerPause") Coroutine { ->
|
|
226
|
+
appRemoteCoordinator.playerPause()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
AsyncFunction("playerResume") Coroutine { ->
|
|
230
|
+
appRemoteCoordinator.playerResume()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
AsyncFunction("playerSkipNext") Coroutine { ->
|
|
234
|
+
appRemoteCoordinator.playerSkipNext()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
AsyncFunction("playerSkipPrevious") Coroutine { ->
|
|
238
|
+
appRemoteCoordinator.playerSkipPrevious()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
AsyncFunction("playerSeekTo") Coroutine { positionMs: Long ->
|
|
242
|
+
appRemoteCoordinator.playerSeekTo(positionMs)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
AsyncFunction("playerSetShuffle") Coroutine { enabled: Boolean ->
|
|
246
|
+
appRemoteCoordinator.playerSetShuffle(enabled)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
AsyncFunction("playerSetRepeatMode") Coroutine { mode: Int ->
|
|
250
|
+
appRemoteCoordinator.playerSetRepeatMode(mode)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
AsyncFunction("playerSetPodcastPlaybackSpeed") Coroutine { value: Double ->
|
|
254
|
+
appRemoteCoordinator.playerSetPodcastPlaybackSpeed(value.toFloat())
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
AsyncFunction("playerQueue") Coroutine { uri: String ->
|
|
258
|
+
appRemoteCoordinator.playerQueue(uri)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
AsyncFunction("playerGetPlayerState") Coroutine { ->
|
|
262
|
+
appRemoteCoordinator.playerGetPlayerState()
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
AsyncFunction("playerGetCrossfadeState") Coroutine { ->
|
|
266
|
+
appRemoteCoordinator.playerGetCrossfadeState()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── User ────────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
AsyncFunction("userGetCapabilities") Coroutine { ->
|
|
272
|
+
appRemoteCoordinator.userGetCapabilities()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
AsyncFunction("userGetLibraryState") Coroutine { uri: String ->
|
|
276
|
+
appRemoteCoordinator.userGetLibraryState(uri)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
AsyncFunction("userAddToLibrary") Coroutine { uri: String ->
|
|
280
|
+
appRemoteCoordinator.userAddToLibrary(uri)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
AsyncFunction("userRemoveFromLibrary") Coroutine { uri: String ->
|
|
284
|
+
appRemoteCoordinator.userRemoveFromLibrary(uri)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ── Content ───────────────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
AsyncFunction("contentGetRecommendedContentItems") Coroutine { type: String ->
|
|
290
|
+
appRemoteCoordinator.contentGetRecommendedContentItems(type)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
AsyncFunction("contentGetChildren") Coroutine { item: Map<String, Any?> ->
|
|
294
|
+
appRemoteCoordinator.contentGetChildren(item)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── Images ────────────────────────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
AsyncFunction("imagesLoad") Coroutine { imageIdentifier: String, size: String ->
|
|
300
|
+
appRemoteCoordinator.imagesLoad(imageIdentifier, size, context.cacheDir)
|
|
301
|
+
}
|
|
165
302
|
}
|
|
166
303
|
}
|