@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.
Files changed (87) hide show
  1. package/README.md +404 -140
  2. package/android/build.gradle +12 -2
  3. package/android/consumer-rules.pro +5 -0
  4. package/android/libs/SETUP.md +29 -0
  5. package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +146 -9
  6. package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteCoordinator.kt +556 -0
  7. package/android/src/main/java/expo/modules/spotifysdk/SpotifyErrors.kt +112 -0
  8. package/app.plugin.js +1 -1
  9. package/build/ExpoSpotifySDKModule.web.d.ts +23 -0
  10. package/build/ExpoSpotifySDKModule.web.d.ts.map +1 -1
  11. package/build/ExpoSpotifySDKModule.web.js +74 -5
  12. package/build/ExpoSpotifySDKModule.web.js.map +1 -1
  13. package/build/app-remote/error.d.ts +8 -0
  14. package/build/app-remote/error.d.ts.map +1 -0
  15. package/build/app-remote/error.js +10 -0
  16. package/build/app-remote/error.js.map +1 -0
  17. package/build/app-remote/index.d.ts +79 -0
  18. package/build/app-remote/index.d.ts.map +1 -0
  19. package/build/app-remote/index.js +110 -0
  20. package/build/app-remote/index.js.map +1 -0
  21. package/build/auth/error.d.ts +8 -0
  22. package/build/auth/error.d.ts.map +1 -0
  23. package/build/auth/error.js +10 -0
  24. package/build/auth/error.js.map +1 -0
  25. package/build/auth/index.d.ts +124 -0
  26. package/build/auth/index.d.ts.map +1 -0
  27. package/build/auth/index.js +162 -0
  28. package/build/auth/index.js.map +1 -0
  29. package/build/content/error.d.ts +8 -0
  30. package/build/content/error.d.ts.map +1 -0
  31. package/build/content/error.js +10 -0
  32. package/build/content/error.js.map +1 -0
  33. package/build/content/index.d.ts +38 -0
  34. package/build/content/index.d.ts.map +1 -0
  35. package/build/content/index.js +56 -0
  36. package/build/content/index.js.map +1 -0
  37. package/build/error.d.ts +24 -0
  38. package/build/error.d.ts.map +1 -0
  39. package/build/error.js +25 -0
  40. package/build/error.js.map +1 -0
  41. package/build/hooks/index.d.ts +90 -0
  42. package/build/hooks/index.d.ts.map +1 -0
  43. package/build/hooks/index.js +326 -0
  44. package/build/hooks/index.js.map +1 -0
  45. package/build/images/error.d.ts +8 -0
  46. package/build/images/error.d.ts.map +1 -0
  47. package/build/images/error.js +10 -0
  48. package/build/images/error.js.map +1 -0
  49. package/build/images/index.d.ts +36 -0
  50. package/build/images/index.d.ts.map +1 -0
  51. package/build/images/index.js +59 -0
  52. package/build/images/index.js.map +1 -0
  53. package/build/index.d.ts +44 -56
  54. package/build/index.d.ts.map +1 -1
  55. package/build/index.js +49 -141
  56. package/build/index.js.map +1 -1
  57. package/build/player/error.d.ts +8 -0
  58. package/build/player/error.d.ts.map +1 -0
  59. package/build/player/error.js +10 -0
  60. package/build/player/error.js.map +1 -0
  61. package/build/player/index.d.ts +140 -0
  62. package/build/player/index.d.ts.map +1 -0
  63. package/build/player/index.js +137 -0
  64. package/build/player/index.js.map +1 -0
  65. package/build/uri/index.d.ts +55 -0
  66. package/build/uri/index.d.ts.map +1 -0
  67. package/build/uri/index.js +57 -0
  68. package/build/uri/index.js.map +1 -0
  69. package/build/user/error.d.ts +8 -0
  70. package/build/user/error.d.ts.map +1 -0
  71. package/build/user/error.js +10 -0
  72. package/build/user/error.js.map +1 -0
  73. package/build/user/index.d.ts +44 -0
  74. package/build/user/index.d.ts.map +1 -0
  75. package/build/user/index.js +97 -0
  76. package/build/user/index.js.map +1 -0
  77. package/ios/ExpoSpotifySDK.podspec +1 -1
  78. package/ios/ExpoSpotifySDKModule.swift +227 -5
  79. package/ios/SpotifyAppRemoteCoordinator.swift +939 -0
  80. package/ios/SpotifyAuthCoordinator.swift +53 -13
  81. package/ios/SpotifyTokenRefreshClient.swift +28 -0
  82. package/package.json +29 -16
  83. package/plugin/build/index.d.ts +20 -3
  84. package/plugin/build/index.js +22 -25
  85. package/plugin/build/types.d.ts +17 -1
  86. package/plugin/build/withSpotifySdkConfig.d.ts +4 -0
  87. package/plugin/build/withSpotifySdkConfig.js +27 -0
@@ -16,10 +16,10 @@ buildscript {
16
16
  }
17
17
 
18
18
  project.android {
19
- compileSdkVersion safeExtGet("compileSdkVersion", 35)
19
+ compileSdkVersion safeExtGet("compileSdkVersion", 36)
20
20
  defaultConfig {
21
21
  minSdkVersion safeExtGet("minSdkVersion", 24)
22
- targetSdkVersion safeExtGet("targetSdkVersion", 35)
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.8.0" // x-release-please-version
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 coordinator = SpotifyAuthCoordinator()
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 emitError(type: String, code: String, message: String) {
66
+ private fun emitSessionError(code: String, message: String) {
62
67
  sendEvent(
63
68
  EVENT_SESSION_CHANGE,
64
- mapOf("type" to type, "error" to mapOf("code" to code, "message" to message)),
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(EVENT_SESSION_CHANGE)
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 = coordinator.authenticate(authLauncher, input)
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, null -> throw UnknownSpotifyException(
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
- emitError("didFail", e.code ?: "UNKNOWN", e.localizedMessage ?: e.code ?: "Unknown error")
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
- emitError("didFail", e.code ?: "UNKNOWN", e.localizedMessage ?: e.code ?: "Unknown error")
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
  }