@wwdrew/expo-spotify-sdk 0.7.1 → 1.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 (85) hide show
  1. package/README.md +388 -108
  2. package/android/build.gradle +10 -0
  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 +149 -12
  6. package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteCoordinator.kt +556 -0
  7. package/android/src/main/java/expo/modules/spotifysdk/SpotifyConfig.kt +1 -0
  8. package/android/src/main/java/expo/modules/spotifysdk/SpotifyErrors.kt +112 -0
  9. package/build/ExpoSpotifySDK.types.d.ts +12 -0
  10. package/build/ExpoSpotifySDK.types.d.ts.map +1 -1
  11. package/build/ExpoSpotifySDK.types.js.map +1 -1
  12. package/build/ExpoSpotifySDKModule.web.d.ts +23 -0
  13. package/build/ExpoSpotifySDKModule.web.d.ts.map +1 -1
  14. package/build/ExpoSpotifySDKModule.web.js +74 -5
  15. package/build/ExpoSpotifySDKModule.web.js.map +1 -1
  16. package/build/app-remote/error.d.ts +8 -0
  17. package/build/app-remote/error.d.ts.map +1 -0
  18. package/build/app-remote/error.js +10 -0
  19. package/build/app-remote/error.js.map +1 -0
  20. package/build/app-remote/index.d.ts +79 -0
  21. package/build/app-remote/index.d.ts.map +1 -0
  22. package/build/app-remote/index.js +110 -0
  23. package/build/app-remote/index.js.map +1 -0
  24. package/build/auth/error.d.ts +8 -0
  25. package/build/auth/error.d.ts.map +1 -0
  26. package/build/auth/error.js +10 -0
  27. package/build/auth/error.js.map +1 -0
  28. package/build/auth/index.d.ts +124 -0
  29. package/build/auth/index.d.ts.map +1 -0
  30. package/build/auth/index.js +162 -0
  31. package/build/auth/index.js.map +1 -0
  32. package/build/content/error.d.ts +8 -0
  33. package/build/content/error.d.ts.map +1 -0
  34. package/build/content/error.js +10 -0
  35. package/build/content/error.js.map +1 -0
  36. package/build/content/index.d.ts +38 -0
  37. package/build/content/index.d.ts.map +1 -0
  38. package/build/content/index.js +56 -0
  39. package/build/content/index.js.map +1 -0
  40. package/build/error.d.ts +24 -0
  41. package/build/error.d.ts.map +1 -0
  42. package/build/error.js +25 -0
  43. package/build/error.js.map +1 -0
  44. package/build/hooks/index.d.ts +90 -0
  45. package/build/hooks/index.d.ts.map +1 -0
  46. package/build/hooks/index.js +326 -0
  47. package/build/hooks/index.js.map +1 -0
  48. package/build/images/error.d.ts +8 -0
  49. package/build/images/error.d.ts.map +1 -0
  50. package/build/images/error.js +10 -0
  51. package/build/images/error.js.map +1 -0
  52. package/build/images/index.d.ts +36 -0
  53. package/build/images/index.d.ts.map +1 -0
  54. package/build/images/index.js +59 -0
  55. package/build/images/index.js.map +1 -0
  56. package/build/index.d.ts +44 -42
  57. package/build/index.d.ts.map +1 -1
  58. package/build/index.js +49 -122
  59. package/build/index.js.map +1 -1
  60. package/build/player/error.d.ts +8 -0
  61. package/build/player/error.d.ts.map +1 -0
  62. package/build/player/error.js +10 -0
  63. package/build/player/error.js.map +1 -0
  64. package/build/player/index.d.ts +140 -0
  65. package/build/player/index.d.ts.map +1 -0
  66. package/build/player/index.js +136 -0
  67. package/build/player/index.js.map +1 -0
  68. package/build/uri/index.d.ts +55 -0
  69. package/build/uri/index.d.ts.map +1 -0
  70. package/build/uri/index.js +57 -0
  71. package/build/uri/index.js.map +1 -0
  72. package/build/user/error.d.ts +8 -0
  73. package/build/user/error.d.ts.map +1 -0
  74. package/build/user/error.js +10 -0
  75. package/build/user/error.js.map +1 -0
  76. package/build/user/index.d.ts +44 -0
  77. package/build/user/index.d.ts.map +1 -0
  78. package/build/user/index.js +97 -0
  79. package/build/user/index.js.map +1 -0
  80. package/ios/ExpoSpotifySDKModule.swift +233 -6
  81. package/ios/SPTScopeSerializer.swift +5 -0
  82. package/ios/SpotifyAppRemoteCoordinator.swift +939 -0
  83. package/ios/SpotifyAuthCoordinator.swift +67 -13
  84. package/ios/SpotifyTokenRefreshClient.swift +32 -1
  85. package/package.json +4 -3
@@ -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.5.0"
11
+ private const val SDK_VERSION = "1.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(
@@ -122,20 +151,20 @@ class ExpoSpotifySDKModule : Module() {
122
151
  )
123
152
  }
124
153
  AuthorizationResponse.Type.CANCELLED -> throw UserCancelledException()
125
- AuthorizationResponse.Type.EMPTY -> throw UnknownSpotifyException(
126
- "Spotify returned an empty response (auth activity may have been killed)",
154
+ AuthorizationResponse.Type.EMPTY -> throw UserCancelledException(
155
+ "Spotify returned an empty response (auth activity dismissed before completion)",
127
156
  )
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
  }
@@ -153,14 +182,122 @@ class ExpoSpotifySDKModule : Module() {
153
182
  val payload = client.refresh(
154
183
  refreshToken = options.refreshToken,
155
184
  tokenRefreshURL = options.tokenRefreshURL,
156
- previousScopes = emptyList(),
185
+ previousScopes = options.scopes,
157
186
  )
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
  }