@wwdrew/expo-spotify-sdk 2.0.0 → 2.2.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 (126) hide show
  1. package/README.md +94 -346
  2. package/android/build.gradle +4 -5
  3. package/android/libs/SETUP.md +9 -24
  4. package/android/spotify-native-sdk.gradle +118 -0
  5. package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +14 -5
  6. package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteCoordinator.kt +38 -64
  7. package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteErrorMapping.kt +106 -0
  8. package/android/src/main/java/expo/modules/spotifysdk/SpotifyTokenSwapClient.kt +2 -7
  9. package/build/app-remote/index.d.ts +34 -0
  10. package/build/app-remote/index.d.ts.map +1 -1
  11. package/build/app-remote/index.js +47 -24
  12. package/build/app-remote/index.js.map +1 -1
  13. package/build/auth/index.d.ts.map +1 -1
  14. package/build/auth/index.js +17 -34
  15. package/build/auth/index.js.map +1 -1
  16. package/build/content/index.d.ts.map +1 -1
  17. package/build/content/index.js +12 -25
  18. package/build/content/index.js.map +1 -1
  19. package/build/hooks/index.d.ts +1 -1
  20. package/build/hooks/index.d.ts.map +1 -1
  21. package/build/hooks/index.js +38 -115
  22. package/build/hooks/index.js.map +1 -1
  23. package/build/images/index.d.ts +2 -2
  24. package/build/images/index.d.ts.map +1 -1
  25. package/build/images/index.js +12 -25
  26. package/build/images/index.js.map +1 -1
  27. package/build/index.d.ts +0 -18
  28. package/build/index.d.ts.map +1 -1
  29. package/build/index.js +0 -27
  30. package/build/index.js.map +1 -1
  31. package/build/internal/native-errors.d.ts +14 -0
  32. package/build/internal/native-errors.d.ts.map +1 -0
  33. package/build/internal/native-errors.js +29 -0
  34. package/build/internal/native-errors.js.map +1 -0
  35. package/build/internal/sync-external-store.d.ts +14 -0
  36. package/build/internal/sync-external-store.d.ts.map +1 -0
  37. package/build/internal/sync-external-store.js +36 -0
  38. package/build/internal/sync-external-store.js.map +1 -0
  39. package/build/player/index.d.ts.map +1 -1
  40. package/build/player/index.js +14 -27
  41. package/build/player/index.js.map +1 -1
  42. package/build/user/index.d.ts +2 -2
  43. package/build/user/index.d.ts.map +1 -1
  44. package/build/user/index.js +16 -26
  45. package/build/user/index.js.map +1 -1
  46. package/ios/ExpoSpotifyAppDelegate.swift +8 -1
  47. package/ios/ExpoSpotifySDK.podspec +26 -2
  48. package/ios/ExpoSpotifySDKModule.swift +16 -1
  49. package/ios/SpotifyAppRemoteCoordinator.swift +137 -404
  50. package/ios/SpotifyAppRemoteErrorMapping.swift +110 -0
  51. package/ios/SpotifyAppRemoteErrors.swift +221 -0
  52. package/ios/SpotifyAppRemoteMappers.swift +88 -0
  53. package/ios/spotify-native-sdk-versions.json +12 -0
  54. package/package.json +13 -5
  55. package/plugin/index.d.ts +2 -0
  56. package/plugin/index.js +1 -0
  57. package/ios/SpotifySDK/Licenses/MPMessagePack-LICENSE.md +0 -20
  58. package/ios/SpotifySDK/SpotifyiOS.xcframework/Info.plist +0 -40
  59. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemote.h +0 -287
  60. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteAlbum.h +0 -18
  61. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteArtist.h +0 -18
  62. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteCommon.h +0 -37
  63. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectionParams.h +0 -65
  64. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityAPI.h +0 -55
  65. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityState.h +0 -15
  66. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteContentAPI.h +0 -78
  67. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteContentItem.h +0 -49
  68. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteCrossfadeState.h +0 -18
  69. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteImageAPI.h +0 -25
  70. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteImageRepresentable.h +0 -16
  71. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteLibraryState.h +0 -19
  72. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackOptions.h +0 -32
  73. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackRestrictions.h +0 -32
  74. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlayerAPI.h +0 -267
  75. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlayerState.h +0 -40
  76. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePodcastPlaybackSpeed.h +0 -15
  77. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteTrack.h +0 -52
  78. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteUserAPI.h +0 -102
  79. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteUserCapabilities.h +0 -15
  80. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTConfiguration.h +0 -66
  81. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTError.h +0 -31
  82. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTLogin.h +0 -10
  83. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTMacros.h +0 -125
  84. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTScope.h +0 -75
  85. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTSession.h +0 -33
  86. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTSessionManager.h +0 -170
  87. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SpotifyAppRemote.h +0 -28
  88. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SpotifyiOS.h +0 -7
  89. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Info.plist +0 -0
  90. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Modules/module.modulemap +0 -5
  91. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/PrivacyInfo.xcprivacy +0 -14
  92. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/SpotifyiOS +0 -0
  93. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemote.h +0 -287
  94. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteAlbum.h +0 -18
  95. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteArtist.h +0 -18
  96. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteCommon.h +0 -37
  97. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectionParams.h +0 -65
  98. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityAPI.h +0 -55
  99. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityState.h +0 -15
  100. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteContentAPI.h +0 -78
  101. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteContentItem.h +0 -49
  102. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteCrossfadeState.h +0 -18
  103. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteImageAPI.h +0 -25
  104. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteImageRepresentable.h +0 -16
  105. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteLibraryState.h +0 -19
  106. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackOptions.h +0 -32
  107. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackRestrictions.h +0 -32
  108. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlayerAPI.h +0 -267
  109. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlayerState.h +0 -40
  110. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePodcastPlaybackSpeed.h +0 -15
  111. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteTrack.h +0 -52
  112. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteUserAPI.h +0 -102
  113. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteUserCapabilities.h +0 -15
  114. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTConfiguration.h +0 -66
  115. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTError.h +0 -31
  116. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTLogin.h +0 -10
  117. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTMacros.h +0 -125
  118. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTScope.h +0 -75
  119. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTSession.h +0 -33
  120. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTSessionManager.h +0 -170
  121. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SpotifyAppRemote.h +0 -28
  122. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SpotifyiOS.h +0 -7
  123. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Info.plist +0 -0
  124. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Modules/module.modulemap +0 -5
  125. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/PrivacyInfo.xcprivacy +0 -14
  126. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/SpotifyiOS +0 -0
@@ -0,0 +1,118 @@
1
+ // Downloads Spotify App Remote AAR at Gradle build time (pinned in ios/spotify-native-sdk-versions.json).
2
+ def spotifyNativeVersionsFile = file("${projectDir}/../ios/spotify-native-sdk-versions.json")
3
+ if (!spotifyNativeVersionsFile.exists()) {
4
+ throw new GradleException(
5
+ "Missing ${spotifyNativeVersionsFile}. Native SDK versions must be pinned in ios/spotify-native-sdk-versions.json."
6
+ )
7
+ }
8
+
9
+ def spotifyNative = new groovy.json.JsonSlurper().parse(spotifyNativeVersionsFile)
10
+ def androidSdk = spotifyNative.android
11
+ if (androidSdk == null) {
12
+ throw new GradleException("ios/spotify-native-sdk-versions.json is missing an \"android\" section.")
13
+ }
14
+
15
+ def appRemoteVersion = androidSdk.appRemoteVersion as String
16
+ def appRemoteReleaseTag = androidSdk.appRemoteReleaseTag as String
17
+ def appRemoteSha256 = androidSdk.appRemoteSha256 as String
18
+
19
+ if (!appRemoteVersion || !appRemoteReleaseTag || !appRemoteSha256) {
20
+ throw new GradleException(
21
+ "ios/spotify-native-sdk-versions.json android section is missing required fields: " +
22
+ "appRemoteVersion, appRemoteReleaseTag, appRemoteSha256"
23
+ )
24
+ }
25
+
26
+ def libsDir = file("${projectDir}/libs")
27
+ def aarFile = file("${libsDir}/spotify-app-remote-release-${appRemoteVersion}.aar")
28
+ def versionMarker = file("${libsDir}/.version")
29
+ def downloadUrl =
30
+ "https://github.com/spotify/android-sdk/releases/download/${appRemoteReleaseTag}/spotify-app-remote-release-${appRemoteVersion}.aar"
31
+
32
+ def sha256File = { File f ->
33
+ def digest = java.security.MessageDigest.getInstance("SHA-256")
34
+ f.withInputStream { stream ->
35
+ def buffer = new byte[8192]
36
+ int read
37
+ while ((read = stream.read(buffer)) > 0) {
38
+ digest.update(buffer, 0, read)
39
+ }
40
+ }
41
+ digest.digest().collect { String.format("%02x", it) }.join("")
42
+ }
43
+
44
+ def appRemoteAarUpToDate = {
45
+ if (!aarFile.exists()) {
46
+ return false
47
+ }
48
+ if (!versionMarker.exists() || versionMarker.text.trim() != appRemoteVersion) {
49
+ return false
50
+ }
51
+ sha256File(aarFile) == appRemoteSha256
52
+ }
53
+
54
+ tasks.register("downloadSpotifyAppRemoteAar") {
55
+ group = "spotify"
56
+ description = "Download Spotify App Remote AAR from GitHub releases"
57
+
58
+ outputs.file(aarFile)
59
+ outputs.file(versionMarker)
60
+
61
+ onlyIf {
62
+ !appRemoteAarUpToDate()
63
+ }
64
+
65
+ doLast {
66
+ libsDir.mkdirs()
67
+ logger.lifecycle("ExpoSpotifySDK: downloading Spotify App Remote ${appRemoteVersion}")
68
+
69
+ def tmp = File.createTempFile("spotify-app-remote", ".aar")
70
+ try {
71
+ ant.get(
72
+ src: downloadUrl,
73
+ dest: tmp,
74
+ verbose: true,
75
+ connecttimeout: 30000,
76
+ readtimeout: 60000
77
+ )
78
+ def actual = sha256File(tmp)
79
+ if (actual != appRemoteSha256) {
80
+ throw new GradleException(
81
+ "Spotify App Remote AAR checksum mismatch\n" +
82
+ " expected ${appRemoteSha256}\n" +
83
+ " actual ${actual}"
84
+ )
85
+ }
86
+
87
+ libsDir.listFiles()?.each { f ->
88
+ if (f.name.endsWith(".aar") && f.name != aarFile.name) {
89
+ f.delete()
90
+ }
91
+ }
92
+
93
+ if (aarFile.exists()) {
94
+ aarFile.delete()
95
+ }
96
+
97
+ if (!tmp.renameTo(aarFile)) {
98
+ tmp.withInputStream { input ->
99
+ aarFile.withOutputStream { output ->
100
+ output << input
101
+ }
102
+ }
103
+ }
104
+
105
+ versionMarker.text = appRemoteVersion
106
+ } finally {
107
+ tmp.delete()
108
+ }
109
+
110
+ logger.lifecycle("ExpoSpotifySDK: Spotify App Remote AAR ready at ${aarFile}")
111
+ }
112
+ }
113
+
114
+ ext.spotifyAppRemoteAarFile = aarFile
115
+
116
+ tasks.named("preBuild").configure {
117
+ dependsOn("downloadSpotifyAppRemoteAar")
118
+ }
@@ -8,7 +8,7 @@ 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 = "2.0.0" // x-release-please-version
11
+ private const val SDK_VERSION = "2.2.0" // x-release-please-version
12
12
  private const val EVENT_SESSION_CHANGE = "onSessionChange"
13
13
  private const val EVENT_CONNECTION_STATE_CHANGE = "onConnectionStateChange"
14
14
  private const val EVENT_CONNECTION_ERROR = "onConnectionError"
@@ -141,11 +141,10 @@ class ExpoSpotifySDKModule : Module() {
141
141
  ?: throw InvalidConfigException(
142
142
  "Received CODE response but no tokenSwapURL was configured",
143
143
  )
144
- val client = SpotifyTokenSwapClient(SDK_VERSION, manifest.clientId)
144
+ val client = SpotifyTokenSwapClient(SDK_VERSION)
145
145
  client.swap(
146
146
  code = response.code
147
147
  ?: throw TokenSwapParseException("Spotify returned CODE without a code"),
148
- redirectUri = manifest.redirectUri,
149
148
  tokenSwapURL = swapURL,
150
149
  requestedScopes = config.scopes,
151
150
  )
@@ -177,8 +176,7 @@ class ExpoSpotifySDKModule : Module() {
177
176
  if (options.tokenRefreshURL.isBlank()) {
178
177
  throw InvalidConfigException("`tokenRefreshURL` is required")
179
178
  }
180
- val manifest = readManifestConfig()
181
- val client = SpotifyTokenSwapClient(SDK_VERSION, manifest.clientId)
179
+ val client = SpotifyTokenSwapClient(SDK_VERSION)
182
180
  val payload = client.refresh(
183
181
  refreshToken = options.refreshToken,
184
182
  tokenRefreshURL = options.tokenRefreshURL,
@@ -204,6 +202,17 @@ class ExpoSpotifySDKModule : Module() {
204
202
  )
205
203
  }
206
204
 
205
+ AsyncFunction("appRemoteAuthorizeAndPlay") Coroutine { accessToken: String, uri: String ->
206
+ val manifest = readManifestConfig()
207
+ appRemoteCoordinator.authorizeAndPlay(
208
+ context = context,
209
+ clientId = manifest.clientId,
210
+ redirectUri = manifest.redirectUri,
211
+ accessToken = accessToken,
212
+ uri = uri,
213
+ )
214
+ }
215
+
207
216
  AsyncFunction("appRemoteDisconnect") Coroutine { ->
208
217
  appRemoteCoordinator.disconnect()
209
218
  }
@@ -18,7 +18,6 @@ import com.spotify.protocol.types.ListItems
18
18
  import com.spotify.protocol.types.PlayerState
19
19
  import com.spotify.protocol.types.PlaybackSpeed
20
20
  import com.spotify.protocol.types.Repeat
21
- import expo.modules.kotlin.exception.CodedException
22
21
  import kotlinx.coroutines.suspendCancellableCoroutine
23
22
  import kotlinx.coroutines.sync.Mutex
24
23
  import java.io.File
@@ -132,6 +131,38 @@ class SpotifyAppRemoteCoordinator {
132
131
  }
133
132
  }
134
133
 
134
+ /**
135
+ * Wakes the Spotify app and starts playback, then ensures an active App
136
+ * Remote connection — the cross-platform counterpart of iOS
137
+ * `authorizeAndPlayURI`.
138
+ *
139
+ * On Android there is no dedicated "authorize and play" entry point:
140
+ * [SpotifyAppRemote.connect] already launches/wakes the Spotify service when
141
+ * it is installed, so this simply connects (if not already connected) and
142
+ * then issues a play (or resume, for an empty [uri]) command. The resulting
143
+ * playback keeps Spotify alive, mirroring the iOS behaviour.
144
+ *
145
+ * @param uri Spotify URI to play, or empty to resume the last/contextual track.
146
+ */
147
+ suspend fun authorizeAndPlay(
148
+ context: Context,
149
+ clientId: String,
150
+ redirectUri: String,
151
+ accessToken: String,
152
+ uri: String,
153
+ ) {
154
+ if (appRemote?.isConnected != true) {
155
+ connect(context, clientId, redirectUri, accessToken)
156
+ }
157
+ if (uri.isEmpty()) {
158
+ requireConnected("AppRemote.authorizeAndPlay").playerApi.resume()
159
+ .awaitVoid("AppRemote.authorizeAndPlay")
160
+ } else {
161
+ requireConnected("AppRemote.authorizeAndPlay").playerApi.play(uri)
162
+ .awaitVoid("AppRemote.authorizeAndPlay")
163
+ }
164
+ }
165
+
135
166
  /**
136
167
  * Disconnects from the Spotify app. Safe to call when already disconnected.
137
168
  */
@@ -336,7 +367,7 @@ class SpotifyAppRemoteCoordinator {
336
367
  suspendCancellableCoroutine { continuation ->
337
368
  setResultCallback { continuation.resume(Unit) }
338
369
  setErrorCallback { throwable ->
339
- continuation.resumeWithException(normalizePlayerError(throwable, callsite))
370
+ continuation.resumeWithException(SpotifyAppRemoteErrorMapping.mapPlayerError(throwable, callsite))
340
371
  }
341
372
  }
342
373
  }
@@ -347,7 +378,7 @@ class SpotifyAppRemoteCoordinator {
347
378
  return suspendCancellableCoroutine { continuation ->
348
379
  setResultCallback { result -> continuation.resume(result) }
349
380
  setErrorCallback { throwable ->
350
- continuation.resumeWithException(normalizePlayerError(throwable, callsite))
381
+ continuation.resumeWithException(SpotifyAppRemoteErrorMapping.mapPlayerError(throwable, callsite))
351
382
  }
352
383
  }
353
384
  }
@@ -357,7 +388,7 @@ class SpotifyAppRemoteCoordinator {
357
388
  return suspendCancellableCoroutine { continuation ->
358
389
  setResultCallback { result -> continuation.resume(result) }
359
390
  setErrorCallback { throwable ->
360
- continuation.resumeWithException(normalizeUserError(throwable, callsite, uri))
391
+ continuation.resumeWithException(SpotifyAppRemoteErrorMapping.mapUserError(throwable, callsite, uri))
361
392
  }
362
393
  }
363
394
  }
@@ -366,7 +397,7 @@ class SpotifyAppRemoteCoordinator {
366
397
  return suspendCancellableCoroutine { continuation ->
367
398
  setResultCallback { result -> continuation.resume(result) }
368
399
  setErrorCallback { throwable ->
369
- continuation.resumeWithException(normalizeUserError(throwable, callsite, ""))
400
+ continuation.resumeWithException(SpotifyAppRemoteErrorMapping.mapUserError(throwable, callsite, ""))
370
401
  }
371
402
  }
372
403
  }
@@ -375,7 +406,7 @@ class SpotifyAppRemoteCoordinator {
375
406
  return suspendCancellableCoroutine { continuation ->
376
407
  setResultCallback { result -> continuation.resume(result) }
377
408
  setErrorCallback { throwable ->
378
- continuation.resumeWithException(normalizeContentError(throwable, callsite))
409
+ continuation.resumeWithException(SpotifyAppRemoteErrorMapping.mapContentError(throwable, callsite))
379
410
  }
380
411
  }
381
412
  }
@@ -384,68 +415,11 @@ class SpotifyAppRemoteCoordinator {
384
415
  return suspendCancellableCoroutine { continuation ->
385
416
  setResultCallback { result -> continuation.resume(result) }
386
417
  setErrorCallback { throwable ->
387
- continuation.resumeWithException(normalizeImagesError(throwable, callsite))
418
+ continuation.resumeWithException(SpotifyAppRemoteErrorMapping.mapImagesError(throwable, callsite))
388
419
  }
389
420
  }
390
421
  }
391
422
 
392
- private fun normalizePlayerError(throwable: Throwable, callsite: String): CodedException {
393
- val msg = throwable.message ?: "Unknown error"
394
- return when {
395
- msg.contains("premium", ignoreCase = true) -> PlayerPremiumRequiredException(callsite)
396
- msg.contains("disconnected", ignoreCase = true) ||
397
- msg.contains("not connected", ignoreCase = true) ->
398
- PlayerConnectionLostException("$callsite: $msg", throwable)
399
- msg.contains("not allowed", ignoreCase = true) ||
400
- msg.contains("restriction", ignoreCase = true) ->
401
- PlayerOperationNotAllowedException("$callsite: $msg", throwable)
402
- else -> PlayerUnknownException("$callsite: $msg", throwable)
403
- }
404
- }
405
-
406
- private fun normalizeUserError(throwable: Throwable, callsite: String, uri: String): CodedException {
407
- val msg = throwable.message ?: "Unknown error"
408
- return when {
409
- msg.contains("disconnected", ignoreCase = true) ||
410
- msg.contains("not connected", ignoreCase = true) ->
411
- UserConnectionLostException("$callsite: $msg", throwable)
412
- msg.contains("uri", ignoreCase = true) ||
413
- msg.contains("invalid", ignoreCase = true) ->
414
- UserInvalidURIException(uri, throwable)
415
- msg.contains("not allowed", ignoreCase = true) ||
416
- msg.contains("restriction", ignoreCase = true) ->
417
- UserOperationNotAllowedException("$callsite: $msg", throwable)
418
- else -> UserUnknownException("$callsite: $msg", throwable)
419
- }
420
- }
421
-
422
- private fun normalizeContentError(throwable: Throwable, callsite: String): CodedException {
423
- val msg = throwable.message ?: "Unknown error"
424
- return when {
425
- msg.contains("disconnected", ignoreCase = true) ||
426
- msg.contains("not connected", ignoreCase = true) ->
427
- ContentConnectionLostException("$callsite: $msg", throwable)
428
- msg.contains("not supported", ignoreCase = true) ||
429
- msg.contains("unsupported", ignoreCase = true) ->
430
- ContentApiUnavailableException("$callsite: content API unavailable on this Spotify app version", throwable)
431
- else -> ContentUnknownException("$callsite: $msg", throwable)
432
- }
433
- }
434
-
435
- private fun normalizeImagesError(throwable: Throwable, callsite: String): CodedException {
436
- val msg = throwable.message ?: "Unknown error"
437
- return when {
438
- msg.contains("disconnected", ignoreCase = true) ||
439
- msg.contains("not connected", ignoreCase = true) ->
440
- ImagesNotConnectedException(callsite)
441
- msg.contains("invalid", ignoreCase = true) ->
442
- ImagesInvalidURIException("$callsite: invalid image identifier", throwable)
443
- msg.contains("load", ignoreCase = true) || msg.contains("image", ignoreCase = true) ->
444
- ImagesLoadFailedException("$callsite: $msg", throwable)
445
- else -> ImagesUnknownException("$callsite: $msg", throwable)
446
- }
447
- }
448
-
449
423
  private fun writeBitmapToTempFile(bitmap: Bitmap, cacheDir: File): File {
450
424
  val out = File(cacheDir, "expo-spotify-image-${System.currentTimeMillis()}-${(0..9999).random()}.png")
451
425
  FileOutputStream(out).use { stream ->
@@ -0,0 +1,106 @@
1
+ package expo.modules.spotifysdk
2
+
3
+ import com.spotify.android.appremote.api.error.SpotifyConnectionTerminatedException
4
+ import com.spotify.android.appremote.api.error.SpotifyDisconnectedException
5
+ import com.spotify.android.appremote.api.error.SpotifyRemoteServiceException
6
+ import com.spotify.protocol.client.error.RemoteClientException
7
+ import com.spotify.protocol.error.SpotifyAppRemoteException
8
+ import expo.modules.kotlin.exception.CodedException
9
+
10
+ /**
11
+ * Maps Spotify App Remote SDK throwables into per-namespace [CodedException] types.
12
+ *
13
+ * Typed SDK exceptions are checked first; message heuristics are a fallback aligned
14
+ * with [ios/SpotifyAppRemoteErrorMapping.swift]. See [docs/app-remote-error-mapping.md].
15
+ */
16
+ object SpotifyAppRemoteErrorMapping {
17
+ private fun connectionLostMessage(callsite: String): String =
18
+ "$callsite: connection to Spotify app was terminated"
19
+
20
+ private fun isConnectionTerminated(throwable: Throwable): Boolean =
21
+ throwable is SpotifyConnectionTerminatedException ||
22
+ throwable is SpotifyDisconnectedException
23
+
24
+ private fun messageIndicatesConnectionLost(message: String): Boolean {
25
+ val lower = message.lowercase()
26
+ return lower.contains("disconnected") || lower.contains("not connected")
27
+ }
28
+
29
+ private fun containsRestriction(message: String): Boolean {
30
+ val lower = message.lowercase()
31
+ return lower.contains("not allowed") || lower.contains("restriction")
32
+ }
33
+
34
+ /** Request-level failures from the Spotify protocol layer (mirrors iOS `requestFailedError`). */
35
+ private fun isRequestFailed(throwable: Throwable): Boolean =
36
+ throwable is RemoteClientException ||
37
+ throwable is SpotifyAppRemoteException ||
38
+ throwable is SpotifyRemoteServiceException
39
+
40
+ fun mapPlayerError(throwable: Throwable, callsite: String): CodedException {
41
+ val message = throwable.message ?: "Unknown error"
42
+ if (isConnectionTerminated(throwable) || messageIndicatesConnectionLost(message)) {
43
+ return PlayerConnectionLostException(connectionLostMessage(callsite), throwable)
44
+ }
45
+ if (message.contains("premium", ignoreCase = true)) {
46
+ return PlayerPremiumRequiredException(
47
+ "$callsite: Spotify Premium is required for on-demand playback",
48
+ )
49
+ }
50
+ if (containsRestriction(message)) {
51
+ return PlayerOperationNotAllowedException("$callsite: $message", throwable)
52
+ }
53
+ if (isRequestFailed(throwable) && message.contains("invalid", ignoreCase = true)) {
54
+ return PlayerInvalidParameterException("$callsite: $message", throwable)
55
+ }
56
+ return PlayerUnknownException("$callsite: $message", throwable)
57
+ }
58
+
59
+ fun mapUserError(throwable: Throwable, callsite: String, uri: String): CodedException {
60
+ val message = throwable.message ?: "Unknown error"
61
+ if (isConnectionTerminated(throwable) || messageIndicatesConnectionLost(message)) {
62
+ return UserConnectionLostException(connectionLostMessage(callsite), throwable)
63
+ }
64
+ if (
65
+ isRequestFailed(throwable) &&
66
+ (message.contains("uri", ignoreCase = true) || message.contains("invalid", ignoreCase = true))
67
+ ) {
68
+ return UserInvalidURIException(uri, throwable)
69
+ }
70
+ if (containsRestriction(message)) {
71
+ return UserOperationNotAllowedException("$callsite: $message", throwable)
72
+ }
73
+ return UserUnknownException("$callsite: $message", throwable)
74
+ }
75
+
76
+ fun mapContentError(throwable: Throwable, callsite: String): CodedException {
77
+ val message = throwable.message ?: "Unknown error"
78
+ if (isConnectionTerminated(throwable) || messageIndicatesConnectionLost(message)) {
79
+ return ContentConnectionLostException(connectionLostMessage(callsite), throwable)
80
+ }
81
+ if (isRequestFailed(throwable)) {
82
+ val lower = message.lowercase()
83
+ if (lower.contains("not supported") || lower.contains("unsupported")) {
84
+ return ContentApiUnavailableException(
85
+ "$callsite: content API is unavailable on this Spotify app version",
86
+ throwable,
87
+ )
88
+ }
89
+ }
90
+ return ContentUnknownException("$callsite: $message", throwable)
91
+ }
92
+
93
+ fun mapImagesError(throwable: Throwable, callsite: String): CodedException {
94
+ val message = throwable.message ?: "Unknown error"
95
+ if (isConnectionTerminated(throwable) || messageIndicatesConnectionLost(message)) {
96
+ return ImagesNotConnectedException(connectionLostMessage(callsite))
97
+ }
98
+ if (isRequestFailed(throwable) && message.contains("invalid", ignoreCase = true)) {
99
+ return ImagesInvalidURIException("$callsite: invalid image identifier", throwable)
100
+ }
101
+ if (isRequestFailed(throwable)) {
102
+ return ImagesLoadFailedException("$callsite: Spotify rejected image request", throwable)
103
+ }
104
+ return ImagesUnknownException("$callsite: $message", throwable)
105
+ }
106
+ }
@@ -20,16 +20,15 @@ import kotlin.coroutines.resumeWithException
20
20
  *
21
21
  * Server contract:
22
22
  * POST {tokenSwapURL}
23
- * body: code=<auth-code>&redirect_uri=<redirect-uri>&client_id=<client-id>
23
+ * body: code=<auth-code>
24
24
  * 200 -> { access_token, expires_in, refresh_token?, scope? }
25
25
  *
26
26
  * POST {tokenRefreshURL}
27
- * body: refresh_token=<token>&client_id=<client-id>
27
+ * body: refresh_token=<token>
28
28
  * 200 -> { access_token, expires_in, refresh_token?, scope? }
29
29
  */
30
30
  class SpotifyTokenSwapClient(
31
31
  private val sdkVersion: String,
32
- private val clientId: String,
33
32
  ) {
34
33
  private val client: OkHttpClient = OkHttpClient.Builder()
35
34
  .connectTimeout(30, TimeUnit.SECONDS)
@@ -41,14 +40,11 @@ class SpotifyTokenSwapClient(
41
40
 
42
41
  suspend fun swap(
43
42
  code: String,
44
- redirectUri: String,
45
43
  tokenSwapURL: String,
46
44
  requestedScopes: List<String>,
47
45
  ): SpotifySessionPayload {
48
46
  val body = FormBody.Builder()
49
47
  .add("code", code)
50
- .add("redirect_uri", redirectUri)
51
- .add("client_id", clientId)
52
48
  .build()
53
49
  val request = Request.Builder()
54
50
  .url(tokenSwapURL)
@@ -66,7 +62,6 @@ class SpotifyTokenSwapClient(
66
62
  ): SpotifySessionPayload {
67
63
  val body = FormBody.Builder()
68
64
  .add("refresh_token", refreshToken)
69
- .add("client_id", clientId)
70
65
  .build()
71
66
  val request = Request.Builder()
72
67
  .url(tokenRefreshURL)
@@ -48,6 +48,40 @@ export declare const AppRemote: {
48
48
  * Calling `connect()` while already connected is a no-op.
49
49
  */
50
50
  readonly connect: (accessToken: string) => Promise<void>;
51
+ /**
52
+ * Wakes the Spotify app — launching it if it has been suspended in the
53
+ * background — starts playback, and then establishes the App Remote
54
+ * connection. This is the recommended way to (re)connect when Spotify may
55
+ * not currently be running.
56
+ *
57
+ * Why this exists: {@link connect} only attaches to an *already-running*
58
+ * Spotify process. If Spotify has been suspended (e.g. iOS reclaims a
59
+ * backgrounded app that isn't playing audio, or the connection dropped
60
+ * ~30s after the user paused), `connect()` rejects with `CONNECTION_FAILED`
61
+ * and there is no way to revive Spotify from `connect()` alone.
62
+ * `authorizeAndPlay()` performs an app-switch to Spotify
63
+ * (iOS `authorizeAndPlayURI`; Android wakes the App Remote service and
64
+ * issues a play command), which both revives Spotify and keeps it alive by
65
+ * starting audio.
66
+ *
67
+ * Trade-offs:
68
+ * - It **always starts (or resumes) playback** — it cannot wake Spotify
69
+ * silently.
70
+ * - It briefly **switches to the Spotify app** and back; the OS does not
71
+ * allow waking another app without a foreground app-switch.
72
+ * - Requires Spotify Premium for on-demand playback of a specific `uri`.
73
+ *
74
+ * @param accessToken Access token from `Auth.authenticate()`. On Android this
75
+ * is an iOS-parity parameter (see {@link connect}); on iOS the token
76
+ * returned by the authorization redirect is used to complete the connection.
77
+ * @param uri Spotify URI to play. Pass an empty string (the default) to
78
+ * resume the user's last track or play a contextual/random track.
79
+ *
80
+ * Resolves once Spotify is connected and the play command has been issued;
81
+ * rejects with an {@link AppRemoteError} if Spotify is not installed or the
82
+ * authorization is denied.
83
+ */
84
+ readonly authorizeAndPlay: (accessToken: string, uri?: string) => Promise<void>;
51
85
  /**
52
86
  * Disconnects from the Spotify app. Safe to call when already disconnected.
53
87
  * Resolves once the disconnection is complete.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/app-remote/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG3D,OAAO,EAAkB,KAAK,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElE,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAMzC,8DAA8D;AAC9D,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,CAAC;AAE1E,oDAAoD;AACpD,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,eAAe,CAAC;CACxB;AAED,8CAA8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,kBAAkB,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;CACjB;AAqCD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,SAAS;IACpB;;;;;;;;;;;OAWG;oCACkB,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;IAM3C;;;OAGG;+BACW,OAAO,CAAC,IAAI,CAAC;IAI3B;;;;OAIG;gCACY,OAAO;IAItB;;;OAGG;uCACmB,OAAO,CAAC,eAAe,CAAC;IAI9C;;;;;;;;;OASG;2BACS,CAAC,SAAS,uBAAuB,GAAG,iBAAiB,SACxD,CAAC,YACE,CAAC,SAAS,uBAAuB,GACvC,CAAC,KAAK,EAAE,0BAA0B,KAAK,IAAI,GAC3C,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,KACxC,iBAAiB;CAUZ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/app-remote/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG3D,OAAO,EAAkB,KAAK,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAGlE,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAMzC,8DAA8D;AAC9D,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,CAAC;AAE1E,oDAAoD;AACpD,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,eAAe,CAAC;CACxB;AAED,8CAA8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,kBAAkB,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;CACjB;AAqBD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,SAAS;IACpB;;;;;;;;;;;OAWG;oCACkB,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;IAM3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;6CAC2B,MAAM,QAAO,MAAM,KAAQ,OAAO,CAAC,IAAI,CAAC;IAOtE;;;OAGG;+BACW,OAAO,CAAC,IAAI,CAAC;IAI3B;;;;OAIG;gCACY,OAAO;IAItB;;;OAGG;uCACmB,OAAO,CAAC,eAAe,CAAC;IAI9C;;;;;;;;;OASG;2BACS,CAAC,SAAS,uBAAuB,GAAG,iBAAiB,SACxD,CAAC,YACE,CAAC,SAAS,uBAAuB,GACvC,CAAC,KAAK,EAAE,0BAA0B,KAAK,IAAI,GAC3C,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,KACxC,iBAAiB;CAUZ,CAAC"}
@@ -1,33 +1,20 @@
1
1
  import ExpoSpotifySDKModule from "../ExpoSpotifySDKModule";
2
2
  import { AppRemoteError } from "./error";
3
+ import { createNativeErrorRethrow } from "../internal/native-errors";
3
4
  export { AppRemoteError } from "./error";
4
5
  // ---------------------------------------------------------------------------
5
6
  // Internal helpers
6
7
  // ---------------------------------------------------------------------------
7
- const VALID_APPREMOTE_CODES = new Set([
8
- "CONNECTION_FAILED",
9
- "CONNECTION_LOST",
10
- "NOT_CONNECTED",
11
- "UNKNOWN",
12
- ]);
13
- const CAUSE_SEPARATOR = "→ Caused by: ";
14
- function unwrapReason(message) {
15
- const idx = message.lastIndexOf(CAUSE_SEPARATOR);
16
- return idx === -1 ? message : message.slice(idx + CAUSE_SEPARATOR.length);
17
- }
18
- function rethrowAsAppRemoteError(err) {
19
- if (err instanceof AppRemoteError)
20
- throw err;
21
- if (err instanceof Error) {
22
- const reason = unwrapReason(err.message);
23
- const maybeCode = err.code;
24
- if (maybeCode && VALID_APPREMOTE_CODES.has(maybeCode)) {
25
- throw new AppRemoteError(maybeCode, reason);
26
- }
27
- throw new AppRemoteError("UNKNOWN", reason);
28
- }
29
- throw new AppRemoteError("UNKNOWN", String(err));
30
- }
8
+ const rethrowAsAppRemoteError = createNativeErrorRethrow({
9
+ ErrorClass: AppRemoteError,
10
+ unknownCode: "UNKNOWN",
11
+ validCodes: new Set([
12
+ "CONNECTION_FAILED",
13
+ "CONNECTION_LOST",
14
+ "NOT_CONNECTED",
15
+ "UNKNOWN",
16
+ ]),
17
+ });
31
18
  // ---------------------------------------------------------------------------
32
19
  // AppRemote namespace
33
20
  // ---------------------------------------------------------------------------
@@ -68,6 +55,42 @@ export const AppRemote = {
68
55
  connect(accessToken) {
69
56
  return ExpoSpotifySDKModule.appRemoteConnect(accessToken).catch(rethrowAsAppRemoteError);
70
57
  },
58
+ /**
59
+ * Wakes the Spotify app — launching it if it has been suspended in the
60
+ * background — starts playback, and then establishes the App Remote
61
+ * connection. This is the recommended way to (re)connect when Spotify may
62
+ * not currently be running.
63
+ *
64
+ * Why this exists: {@link connect} only attaches to an *already-running*
65
+ * Spotify process. If Spotify has been suspended (e.g. iOS reclaims a
66
+ * backgrounded app that isn't playing audio, or the connection dropped
67
+ * ~30s after the user paused), `connect()` rejects with `CONNECTION_FAILED`
68
+ * and there is no way to revive Spotify from `connect()` alone.
69
+ * `authorizeAndPlay()` performs an app-switch to Spotify
70
+ * (iOS `authorizeAndPlayURI`; Android wakes the App Remote service and
71
+ * issues a play command), which both revives Spotify and keeps it alive by
72
+ * starting audio.
73
+ *
74
+ * Trade-offs:
75
+ * - It **always starts (or resumes) playback** — it cannot wake Spotify
76
+ * silently.
77
+ * - It briefly **switches to the Spotify app** and back; the OS does not
78
+ * allow waking another app without a foreground app-switch.
79
+ * - Requires Spotify Premium for on-demand playback of a specific `uri`.
80
+ *
81
+ * @param accessToken Access token from `Auth.authenticate()`. On Android this
82
+ * is an iOS-parity parameter (see {@link connect}); on iOS the token
83
+ * returned by the authorization redirect is used to complete the connection.
84
+ * @param uri Spotify URI to play. Pass an empty string (the default) to
85
+ * resume the user's last track or play a contextual/random track.
86
+ *
87
+ * Resolves once Spotify is connected and the play command has been issued;
88
+ * rejects with an {@link AppRemoteError} if Spotify is not installed or the
89
+ * authorization is denied.
90
+ */
91
+ authorizeAndPlay(accessToken, uri = "") {
92
+ return ExpoSpotifySDKModule.appRemoteAuthorizeAndPlay(accessToken, uri).catch(rethrowAsAppRemoteError);
93
+ },
71
94
  /**
72
95
  * Disconnects from the Spotify app. Safe to call when already disconnected.
73
96
  * Resolves once the disconnection is complete.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/app-remote/index.ts"],"names":[],"mappings":"AAEA,OAAO,oBAAoB,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAA2B,MAAM,SAAS,CAAC;AAGlE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAoBzC,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAqB;IACxD,mBAAmB;IACnB,iBAAiB;IACjB,eAAe;IACf,SAAS;CACV,CAAC,CAAC;AAEH,MAAM,eAAe,GAAG,eAAe,CAAC;AAExC,SAAS,YAAY,CAAC,OAAe;IACnC,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC;IACjD,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;AAC5E,CAAC;AAED,SAAS,uBAAuB,CAAC,GAAY;IAC3C,IAAI,GAAG,YAAY,cAAc;QAAE,MAAM,GAAG,CAAC;IAC7C,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACzC,MAAM,SAAS,GAAI,GAAiC,CAAC,IAAI,CAAC;QAC1D,IAAI,SAAS,IAAI,qBAAqB,CAAC,GAAG,CAAC,SAA+B,CAAC,EAAE,CAAC;YAC5E,MAAM,IAAI,cAAc,CAAC,SAA+B,EAAE,MAAM,CAAC,CAAC;QACpE,CAAC;QACD,MAAM,IAAI,cAAc,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAC9C,CAAC;IACD,MAAM,IAAI,cAAc,CAAC,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;AACnD,CAAC;AAED,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,WAAmB;QACzB,OAAO,oBAAoB,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,KAAK,CAC7D,uBAAuB,CACxB,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,UAAU;QACR,OAAO,oBAAoB,CAAC,mBAAmB,EAAE,CAAC;IACpD,CAAC;IAED;;;;OAIG;IACH,WAAW;QACT,OAAO,oBAAoB,CAAC,oBAAoB,EAAE,CAAC;IACrD,CAAC;IAED;;;OAGG;IACH,kBAAkB;QAChB,OAAO,oBAAoB,CAAC,2BAA2B,EAAE,CAAC;IAC5D,CAAC;IAED;;;;;;;;;OASG;IACH,WAAW,CACT,KAAQ,EACR,QAEyC;QAEzC,MAAM,WAAW,GACf,KAAK,KAAK,uBAAuB;YAC/B,CAAC,CAAC,yBAAyB;YAC3B,CAAC,CAAC,mBAAmB,CAAC;QAC1B,OAAO,oBAAoB,CAAC,WAAW,CACrC,WAAW,EACX,QAAoC,CAChB,CAAC;IACzB,CAAC;CACO,CAAC","sourcesContent":["import type { EventSubscription } from \"expo-modules-core\";\n\nimport ExpoSpotifySDKModule from \"../ExpoSpotifySDKModule\";\nimport { AppRemoteError, type AppRemoteErrorCode } from \"./error\";\n\nexport type { AppRemoteErrorCode } from \"./error\";\nexport { AppRemoteError } from \"./error\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Current state of the IPC connection to the Spotify app. */\nexport type ConnectionState = \"disconnected\" | \"connecting\" | \"connected\";\n\n/** Payload of the `connectionStateChange` event. */\nexport interface ConnectionStateChangeEvent {\n state: ConnectionState;\n}\n\n/** Payload of the `connectionError` event. */\nexport interface ConnectionErrorEvent {\n code: AppRemoteErrorCode;\n message: string;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst VALID_APPREMOTE_CODES = new Set<AppRemoteErrorCode>([\n \"CONNECTION_FAILED\",\n \"CONNECTION_LOST\",\n \"NOT_CONNECTED\",\n \"UNKNOWN\",\n]);\n\nconst CAUSE_SEPARATOR = \"→ Caused by: \";\n\nfunction unwrapReason(message: string): string {\n const idx = message.lastIndexOf(CAUSE_SEPARATOR);\n return idx === -1 ? message : message.slice(idx + CAUSE_SEPARATOR.length);\n}\n\nfunction rethrowAsAppRemoteError(err: unknown): never {\n if (err instanceof AppRemoteError) throw err;\n if (err instanceof Error) {\n const reason = unwrapReason(err.message);\n const maybeCode = (err as Error & { code?: string }).code;\n if (maybeCode && VALID_APPREMOTE_CODES.has(maybeCode as AppRemoteErrorCode)) {\n throw new AppRemoteError(maybeCode as AppRemoteErrorCode, reason);\n }\n throw new AppRemoteError(\"UNKNOWN\", reason);\n }\n throw new AppRemoteError(\"UNKNOWN\", String(err));\n}\n\n// ---------------------------------------------------------------------------\n// AppRemote namespace\n// ---------------------------------------------------------------------------\n\n/**\n * Spotify App Remote namespace. Manages the IPC connection to the running\n * Spotify app. All `Player`, `User`, `Content`, and `Images` calls require an\n * active connection established via `AppRemote.connect()`.\n *\n * @example\n * ```ts\n * import { AppRemote } from \"@wwdrew/expo-spotify-sdk\";\n *\n * // Connect using the access token from Auth.authenticate()\n * await AppRemote.connect(session.accessToken);\n *\n * const sub = AppRemote.addListener(\"connectionStateChange\", ({ state }) => {\n * console.log(\"connection state:\", state);\n * });\n *\n * // later:\n * await AppRemote.disconnect();\n * sub.remove();\n * ```\n */\nexport const AppRemote = {\n /**\n * Opens a connection to the running Spotify app using the provided access\n * token. Resolves when the connection is established; rejects with an\n * {@link AppRemoteError} if the connection fails.\n *\n * **Android note:** The access token is accepted for API parity with iOS but\n * the Android App Remote SDK does not accept it directly — it uses the\n * session cached in the Spotify app from your earlier `Auth.authenticate()`\n * call. Ensure `Auth.authenticate()` has succeeded before calling `connect()`.\n *\n * Calling `connect()` while already connected is a no-op.\n */\n connect(accessToken: string): Promise<void> {\n return ExpoSpotifySDKModule.appRemoteConnect(accessToken).catch(\n rethrowAsAppRemoteError,\n );\n },\n\n /**\n * Disconnects from the Spotify app. Safe to call when already disconnected.\n * Resolves once the disconnection is complete.\n */\n disconnect(): Promise<void> {\n return ExpoSpotifySDKModule.appRemoteDisconnect();\n },\n\n /**\n * Returns `true` if currently connected to the Spotify app.\n * This is a synchronous snapshot — subscribe to `\"connectionStateChange\"`\n * for reactive updates.\n */\n isConnected(): boolean {\n return ExpoSpotifySDKModule.appRemoteIsConnected();\n },\n\n /**\n * Returns the current {@link ConnectionState} synchronously.\n * Subscribe to `\"connectionStateChange\"` for reactive updates.\n */\n getConnectionState(): Promise<ConnectionState> {\n return ExpoSpotifySDKModule.appRemoteGetConnectionState();\n },\n\n /**\n * Subscribes to connection lifecycle events.\n *\n * | Event | Payload | When |\n * |---|---|---|\n * | `\"connectionStateChange\"` | `{ state: ConnectionState }` | State transitions |\n * | `\"connectionError\"` | `{ code, message }` | Connection failures and drops |\n *\n * Returns a `Subscription` — call `.remove()` to unsubscribe.\n */\n addListener<E extends \"connectionStateChange\" | \"connectionError\">(\n event: E,\n listener: E extends \"connectionStateChange\"\n ? (event: ConnectionStateChangeEvent) => void\n : (event: ConnectionErrorEvent) => void,\n ): EventSubscription {\n const nativeEvent =\n event === \"connectionStateChange\"\n ? \"onConnectionStateChange\"\n : \"onConnectionError\";\n return ExpoSpotifySDKModule.addListener(\n nativeEvent,\n listener as (event: unknown) => void,\n ) as EventSubscription;\n },\n} as const;\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/app-remote/index.ts"],"names":[],"mappings":"AAEA,OAAO,oBAAoB,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAA2B,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAGrE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAoBzC,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,MAAM,uBAAuB,GAAG,wBAAwB,CAAC;IACvD,UAAU,EAAE,cAAc;IAC1B,WAAW,EAAE,SAAS;IACtB,UAAU,EAAE,IAAI,GAAG,CAAqB;QACtC,mBAAmB;QACnB,iBAAiB;QACjB,eAAe;QACf,SAAS;KACV,CAAC;CACH,CAAC,CAAC;AAEH,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,WAAmB;QACzB,OAAO,oBAAoB,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,KAAK,CAC7D,uBAAuB,CACxB,CAAC;IACJ,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,gBAAgB,CAAC,WAAmB,EAAE,MAAc,EAAE;QACpD,OAAO,oBAAoB,CAAC,yBAAyB,CACnD,WAAW,EACX,GAAG,CACJ,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;IACnC,CAAC;IAED;;;OAGG;IACH,UAAU;QACR,OAAO,oBAAoB,CAAC,mBAAmB,EAAE,CAAC;IACpD,CAAC;IAED;;;;OAIG;IACH,WAAW;QACT,OAAO,oBAAoB,CAAC,oBAAoB,EAAE,CAAC;IACrD,CAAC;IAED;;;OAGG;IACH,kBAAkB;QAChB,OAAO,oBAAoB,CAAC,2BAA2B,EAAE,CAAC;IAC5D,CAAC;IAED;;;;;;;;;OASG;IACH,WAAW,CACT,KAAQ,EACR,QAEyC;QAEzC,MAAM,WAAW,GACf,KAAK,KAAK,uBAAuB;YAC/B,CAAC,CAAC,yBAAyB;YAC3B,CAAC,CAAC,mBAAmB,CAAC;QAC1B,OAAO,oBAAoB,CAAC,WAAW,CACrC,WAAW,EACX,QAAoC,CAChB,CAAC;IACzB,CAAC;CACO,CAAC","sourcesContent":["import type { EventSubscription } from \"expo-modules-core\";\n\nimport ExpoSpotifySDKModule from \"../ExpoSpotifySDKModule\";\nimport { AppRemoteError, type AppRemoteErrorCode } from \"./error\";\nimport { createNativeErrorRethrow } from \"../internal/native-errors\";\n\nexport type { AppRemoteErrorCode } from \"./error\";\nexport { AppRemoteError } from \"./error\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Current state of the IPC connection to the Spotify app. */\nexport type ConnectionState = \"disconnected\" | \"connecting\" | \"connected\";\n\n/** Payload of the `connectionStateChange` event. */\nexport interface ConnectionStateChangeEvent {\n state: ConnectionState;\n}\n\n/** Payload of the `connectionError` event. */\nexport interface ConnectionErrorEvent {\n code: AppRemoteErrorCode;\n message: string;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst rethrowAsAppRemoteError = createNativeErrorRethrow({\n ErrorClass: AppRemoteError,\n unknownCode: \"UNKNOWN\",\n validCodes: new Set<AppRemoteErrorCode>([\n \"CONNECTION_FAILED\",\n \"CONNECTION_LOST\",\n \"NOT_CONNECTED\",\n \"UNKNOWN\",\n ]),\n});\n\n// ---------------------------------------------------------------------------\n// AppRemote namespace\n// ---------------------------------------------------------------------------\n\n/**\n * Spotify App Remote namespace. Manages the IPC connection to the running\n * Spotify app. All `Player`, `User`, `Content`, and `Images` calls require an\n * active connection established via `AppRemote.connect()`.\n *\n * @example\n * ```ts\n * import { AppRemote } from \"@wwdrew/expo-spotify-sdk\";\n *\n * // Connect using the access token from Auth.authenticate()\n * await AppRemote.connect(session.accessToken);\n *\n * const sub = AppRemote.addListener(\"connectionStateChange\", ({ state }) => {\n * console.log(\"connection state:\", state);\n * });\n *\n * // later:\n * await AppRemote.disconnect();\n * sub.remove();\n * ```\n */\nexport const AppRemote = {\n /**\n * Opens a connection to the running Spotify app using the provided access\n * token. Resolves when the connection is established; rejects with an\n * {@link AppRemoteError} if the connection fails.\n *\n * **Android note:** The access token is accepted for API parity with iOS but\n * the Android App Remote SDK does not accept it directly — it uses the\n * session cached in the Spotify app from your earlier `Auth.authenticate()`\n * call. Ensure `Auth.authenticate()` has succeeded before calling `connect()`.\n *\n * Calling `connect()` while already connected is a no-op.\n */\n connect(accessToken: string): Promise<void> {\n return ExpoSpotifySDKModule.appRemoteConnect(accessToken).catch(\n rethrowAsAppRemoteError,\n );\n },\n\n /**\n * Wakes the Spotify app — launching it if it has been suspended in the\n * background — starts playback, and then establishes the App Remote\n * connection. This is the recommended way to (re)connect when Spotify may\n * not currently be running.\n *\n * Why this exists: {@link connect} only attaches to an *already-running*\n * Spotify process. If Spotify has been suspended (e.g. iOS reclaims a\n * backgrounded app that isn't playing audio, or the connection dropped\n * ~30s after the user paused), `connect()` rejects with `CONNECTION_FAILED`\n * and there is no way to revive Spotify from `connect()` alone.\n * `authorizeAndPlay()` performs an app-switch to Spotify\n * (iOS `authorizeAndPlayURI`; Android wakes the App Remote service and\n * issues a play command), which both revives Spotify and keeps it alive by\n * starting audio.\n *\n * Trade-offs:\n * - It **always starts (or resumes) playback** — it cannot wake Spotify\n * silently.\n * - It briefly **switches to the Spotify app** and back; the OS does not\n * allow waking another app without a foreground app-switch.\n * - Requires Spotify Premium for on-demand playback of a specific `uri`.\n *\n * @param accessToken Access token from `Auth.authenticate()`. On Android this\n * is an iOS-parity parameter (see {@link connect}); on iOS the token\n * returned by the authorization redirect is used to complete the connection.\n * @param uri Spotify URI to play. Pass an empty string (the default) to\n * resume the user's last track or play a contextual/random track.\n *\n * Resolves once Spotify is connected and the play command has been issued;\n * rejects with an {@link AppRemoteError} if Spotify is not installed or the\n * authorization is denied.\n */\n authorizeAndPlay(accessToken: string, uri: string = \"\"): Promise<void> {\n return ExpoSpotifySDKModule.appRemoteAuthorizeAndPlay(\n accessToken,\n uri,\n ).catch(rethrowAsAppRemoteError);\n },\n\n /**\n * Disconnects from the Spotify app. Safe to call when already disconnected.\n * Resolves once the disconnection is complete.\n */\n disconnect(): Promise<void> {\n return ExpoSpotifySDKModule.appRemoteDisconnect();\n },\n\n /**\n * Returns `true` if currently connected to the Spotify app.\n * This is a synchronous snapshot — subscribe to `\"connectionStateChange\"`\n * for reactive updates.\n */\n isConnected(): boolean {\n return ExpoSpotifySDKModule.appRemoteIsConnected();\n },\n\n /**\n * Returns the current {@link ConnectionState} synchronously.\n * Subscribe to `\"connectionStateChange\"` for reactive updates.\n */\n getConnectionState(): Promise<ConnectionState> {\n return ExpoSpotifySDKModule.appRemoteGetConnectionState();\n },\n\n /**\n * Subscribes to connection lifecycle events.\n *\n * | Event | Payload | When |\n * |---|---|---|\n * | `\"connectionStateChange\"` | `{ state: ConnectionState }` | State transitions |\n * | `\"connectionError\"` | `{ code, message }` | Connection failures and drops |\n *\n * Returns a `Subscription` — call `.remove()` to unsubscribe.\n */\n addListener<E extends \"connectionStateChange\" | \"connectionError\">(\n event: E,\n listener: E extends \"connectionStateChange\"\n ? (event: ConnectionStateChangeEvent) => void\n : (event: ConnectionErrorEvent) => void,\n ): EventSubscription {\n const nativeEvent =\n event === \"connectionStateChange\"\n ? \"onConnectionStateChange\"\n : \"onConnectionError\";\n return ExpoSpotifySDKModule.addListener(\n nativeEvent,\n listener as (event: unknown) => void,\n ) as EventSubscription;\n },\n} as const;\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,OAAO,EAAa,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAExD,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAMpC,MAAM,WAAW,cAAc;IAC7B,0BAA0B;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,uDAAuD;IACvD,cAAc,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,MAAM,EAAE,YAAY,EAAE,CAAC;CACxB;AAED,MAAM,MAAM,YAAY,GACpB,kBAAkB,GAClB,0BAA0B,GAC1B,4BAA4B,GAC5B,6BAA6B,GAC7B,oBAAoB,GACpB,WAAW,GACX,uBAAuB,GACvB,6BAA6B,GAC7B,yBAAyB,GACzB,wBAAwB,GACxB,oBAAoB,GACpB,kBAAkB,GAClB,eAAe,GACf,2BAA2B,GAC3B,qBAAqB,GACrB,mBAAmB,GACnB,iBAAiB,GACjB,mBAAmB,CAAC;AAExB,MAAM,WAAW,kBAAkB;IACjC,gEAAgE;IAChE,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,eAAe,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAgFzE;;;;;;;;;GASG;AACH,eAAO,MAAM,IAAI;IACf;;OAEG;gCACY,OAAO;IAItB;;;OAGG;oCACkB,kBAAkB,KAAG,OAAO,CAAC,cAAc,CAAC;IAmBjE;;;;OAIG;+BACa,aAAa,KAAG,OAAO,CAAC,cAAc,CAAC;IAgBvD;;;;;;;OAOG;kCACc,OAAO,CAAC,IAAI,CAAC;IAO9B;;;;;;;;;;;;;;;;;;;OAmBG;kCAEM,eAAe,YACZ,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,KAC5C,iBAAiB;CAMZ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,OAAO,EAAa,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAGxD,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAMpC,MAAM,WAAW,cAAc;IAC7B,0BAA0B;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,uDAAuD;IACvD,cAAc,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,MAAM,EAAE,YAAY,EAAE,CAAC;CACxB;AAED,MAAM,MAAM,YAAY,GACpB,kBAAkB,GAClB,0BAA0B,GAC1B,4BAA4B,GAC5B,6BAA6B,GAC7B,oBAAoB,GACpB,WAAW,GACX,uBAAuB,GACvB,6BAA6B,GAC7B,yBAAyB,GACzB,wBAAwB,GACxB,oBAAoB,GACpB,kBAAkB,GAClB,eAAe,GACf,2BAA2B,GAC3B,qBAAqB,GACrB,mBAAmB,GACnB,iBAAiB,GACjB,mBAAmB,CAAC;AAExB,MAAM,WAAW,kBAAkB;IACjC,gEAAgE;IAChE,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,eAAe,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AA+DzE;;;;;;;;;GASG;AACH,eAAO,MAAM,IAAI;IACf;;OAEG;gCACY,OAAO;IAItB;;;OAGG;oCACkB,kBAAkB,KAAG,OAAO,CAAC,cAAc,CAAC;IAsBjE;;;;OAIG;+BACa,aAAa,KAAG,OAAO,CAAC,cAAc,CAAC;IAgBvD;;;;;;;OAOG;kCACc,OAAO,CAAC,IAAI,CAAC;IAO9B;;;;;;;;;;;;;;;;;;;OAmBG;kCAEM,eAAe,YACZ,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,KAC5C,iBAAiB;CAMZ,CAAC"}