@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.
- package/README.md +94 -346
- package/android/build.gradle +4 -5
- package/android/libs/SETUP.md +9 -24
- package/android/spotify-native-sdk.gradle +118 -0
- package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +14 -5
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteCoordinator.kt +38 -64
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteErrorMapping.kt +106 -0
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyTokenSwapClient.kt +2 -7
- package/build/app-remote/index.d.ts +34 -0
- package/build/app-remote/index.d.ts.map +1 -1
- package/build/app-remote/index.js +47 -24
- package/build/app-remote/index.js.map +1 -1
- package/build/auth/index.d.ts.map +1 -1
- package/build/auth/index.js +17 -34
- package/build/auth/index.js.map +1 -1
- package/build/content/index.d.ts.map +1 -1
- package/build/content/index.js +12 -25
- package/build/content/index.js.map +1 -1
- package/build/hooks/index.d.ts +1 -1
- package/build/hooks/index.d.ts.map +1 -1
- package/build/hooks/index.js +38 -115
- package/build/hooks/index.js.map +1 -1
- package/build/images/index.d.ts +2 -2
- package/build/images/index.d.ts.map +1 -1
- package/build/images/index.js +12 -25
- package/build/images/index.js.map +1 -1
- package/build/index.d.ts +0 -18
- package/build/index.d.ts.map +1 -1
- package/build/index.js +0 -27
- package/build/index.js.map +1 -1
- package/build/internal/native-errors.d.ts +14 -0
- package/build/internal/native-errors.d.ts.map +1 -0
- package/build/internal/native-errors.js +29 -0
- package/build/internal/native-errors.js.map +1 -0
- package/build/internal/sync-external-store.d.ts +14 -0
- package/build/internal/sync-external-store.d.ts.map +1 -0
- package/build/internal/sync-external-store.js +36 -0
- package/build/internal/sync-external-store.js.map +1 -0
- package/build/player/index.d.ts.map +1 -1
- package/build/player/index.js +14 -27
- package/build/player/index.js.map +1 -1
- package/build/user/index.d.ts +2 -2
- package/build/user/index.d.ts.map +1 -1
- package/build/user/index.js +16 -26
- package/build/user/index.js.map +1 -1
- package/ios/ExpoSpotifyAppDelegate.swift +8 -1
- package/ios/ExpoSpotifySDK.podspec +26 -2
- package/ios/ExpoSpotifySDKModule.swift +16 -1
- package/ios/SpotifyAppRemoteCoordinator.swift +137 -404
- package/ios/SpotifyAppRemoteErrorMapping.swift +110 -0
- package/ios/SpotifyAppRemoteErrors.swift +221 -0
- package/ios/SpotifyAppRemoteMappers.swift +88 -0
- package/ios/spotify-native-sdk-versions.json +12 -0
- package/package.json +13 -5
- package/plugin/index.d.ts +2 -0
- package/plugin/index.js +1 -0
- package/ios/SpotifySDK/Licenses/MPMessagePack-LICENSE.md +0 -20
- package/ios/SpotifySDK/SpotifyiOS.xcframework/Info.plist +0 -40
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemote.h +0 -287
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteAlbum.h +0 -18
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteArtist.h +0 -18
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteCommon.h +0 -37
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectionParams.h +0 -65
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityAPI.h +0 -55
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityState.h +0 -15
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteContentAPI.h +0 -78
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteContentItem.h +0 -49
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteCrossfadeState.h +0 -18
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteImageAPI.h +0 -25
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteImageRepresentable.h +0 -16
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteLibraryState.h +0 -19
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackOptions.h +0 -32
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackRestrictions.h +0 -32
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlayerAPI.h +0 -267
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlayerState.h +0 -40
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePodcastPlaybackSpeed.h +0 -15
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteTrack.h +0 -52
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteUserAPI.h +0 -102
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteUserCapabilities.h +0 -15
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTConfiguration.h +0 -66
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTError.h +0 -31
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTLogin.h +0 -10
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTMacros.h +0 -125
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTScope.h +0 -75
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTSession.h +0 -33
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTSessionManager.h +0 -170
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SpotifyAppRemote.h +0 -28
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SpotifyiOS.h +0 -7
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Info.plist +0 -0
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Modules/module.modulemap +0 -5
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/PrivacyInfo.xcprivacy +0 -14
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/SpotifyiOS +0 -0
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemote.h +0 -287
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteAlbum.h +0 -18
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteArtist.h +0 -18
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteCommon.h +0 -37
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectionParams.h +0 -65
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityAPI.h +0 -55
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityState.h +0 -15
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteContentAPI.h +0 -78
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteContentItem.h +0 -49
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteCrossfadeState.h +0 -18
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteImageAPI.h +0 -25
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteImageRepresentable.h +0 -16
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteLibraryState.h +0 -19
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackOptions.h +0 -32
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackRestrictions.h +0 -32
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlayerAPI.h +0 -267
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlayerState.h +0 -40
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePodcastPlaybackSpeed.h +0 -15
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteTrack.h +0 -52
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteUserAPI.h +0 -102
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteUserCapabilities.h +0 -15
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTConfiguration.h +0 -66
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTError.h +0 -31
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTLogin.h +0 -10
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTMacros.h +0 -125
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTScope.h +0 -75
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTSession.h +0 -33
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTSessionManager.h +0 -170
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SpotifyAppRemote.h +0 -28
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SpotifyiOS.h +0 -7
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Info.plist +0 -0
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Modules/module.modulemap +0 -5
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/PrivacyInfo.xcprivacy +0 -14
- 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.
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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;
|
|
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
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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;
|
|
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;
|
|
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"}
|