@wwdrew/expo-spotify-sdk 2.1.0 → 2.2.1
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 +32 -2
- package/android/build.gradle +4 -5
- package/android/libs/SETUP.md +9 -24
- package/android/spotify-native-sdk.gradle +116 -0
- package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +12 -1
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteCoordinator.kt +32 -0
- package/build/app-remote/index.d.ts +9 -0
- package/build/app-remote/index.d.ts.map +1 -1
- package/build/app-remote/index.js +11 -0
- package/build/app-remote/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/SpotifyAppRemoteConnection.swift +221 -0
- package/ios/SpotifyAppRemoteCoordinator.swift +14 -114
- package/ios/SpotifyAuthCoordinator.swift +3 -8
- package/ios/SpotifyMainThread.swift +14 -0
- package/ios/spotify-native-sdk-versions.json +12 -0
- package/package.json +10 -4
- 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,221 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SpotifyiOS
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
// MARK: — Connection models
|
|
6
|
+
|
|
7
|
+
enum AppRemoteConnectionKickoff: Equatable {
|
|
8
|
+
case directConnect
|
|
9
|
+
case authorizeAndPlay(uri: String)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
struct AppRemoteConnectionAttempt {
|
|
13
|
+
let remote: SPTAppRemote
|
|
14
|
+
let kickoff: AppRemoteConnectionKickoff
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// MARK: — Connection lifecycle
|
|
18
|
+
|
|
19
|
+
extension SpotifyAppRemoteCoordinator {
|
|
20
|
+
func connect(accessToken: String) async throws {
|
|
21
|
+
if appRemote?.isConnected == true { return }
|
|
22
|
+
try await beginConnection(accessToken: accessToken, kickoff: .directConnect)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Wakes the Spotify app (via `authorizeAndPlayURI`), starts playback, and
|
|
26
|
+
/// then completes the App Remote connection once Spotify redirects back.
|
|
27
|
+
///
|
|
28
|
+
/// Unlike `connect()`, this works even when the Spotify app has been
|
|
29
|
+
/// suspended: `authorizeAndPlayURI` performs an app-switch to Spotify, which
|
|
30
|
+
/// revives it. Spotify then redirects back to the host app with an access
|
|
31
|
+
/// token; `handleAuthorizeRedirect(_:)` consumes that redirect and calls
|
|
32
|
+
/// `connect()` on the same `SPTAppRemote` instance.
|
|
33
|
+
///
|
|
34
|
+
/// The `connectContinuation` is reused for the whole flow, so `didConnect()`
|
|
35
|
+
/// / `didFailToConnect(error:)` resolve it exactly as for `connect()`.
|
|
36
|
+
func authorizeAndPlay(uri: String, accessToken: String) async throws {
|
|
37
|
+
if appRemote?.isConnected == true {
|
|
38
|
+
if uri.isEmpty {
|
|
39
|
+
try await playerResume()
|
|
40
|
+
} else {
|
|
41
|
+
try await playerPlay(uri: uri)
|
|
42
|
+
}
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
try await beginConnection(
|
|
46
|
+
accessToken: accessToken,
|
|
47
|
+
kickoff: .authorizeAndPlay(uri: uri),
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Consumes a redirect URL produced by an `authorizeAndPlay()` app-switch.
|
|
52
|
+
/// Returns `true` if an authorize flow was pending and the URL was handled
|
|
53
|
+
/// (so the AppDelegate should not also forward it to the auth coordinator).
|
|
54
|
+
nonisolated func handleAuthorizeRedirect(_ url: URL) -> Bool {
|
|
55
|
+
guard case .authorizeAndPlay = connectionAttempt?.kickoff,
|
|
56
|
+
let remote = connectionAttempt?.remote
|
|
57
|
+
else {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return SpotifyMainThread.run {
|
|
62
|
+
let params = remote.authorizationParameters(from: url)
|
|
63
|
+
if let token = params?[SPTAppRemoteAccessTokenKey] {
|
|
64
|
+
remote.connectionParameters.accessToken = token
|
|
65
|
+
clearConnectionAttempt()
|
|
66
|
+
remote.connect()
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
if let errorDescription = params?[SPTAppRemoteErrorDescriptionKey] {
|
|
70
|
+
clearConnectionAttempt()
|
|
71
|
+
Task {
|
|
72
|
+
await self.didFailToConnect(error: NSError(
|
|
73
|
+
domain: "ExpoSpotifySDK",
|
|
74
|
+
code: -2,
|
|
75
|
+
userInfo: [NSLocalizedDescriptionKey: errorDescription]
|
|
76
|
+
))
|
|
77
|
+
}
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func disconnect() {
|
|
85
|
+
if let cont = connectContinuation {
|
|
86
|
+
connectContinuation = nil
|
|
87
|
+
cont.resume(throwing: AppRemoteError.connectionFailed("Disconnected before connection completed"))
|
|
88
|
+
}
|
|
89
|
+
teardownPlayerSubscription()
|
|
90
|
+
teardownCapabilitiesSubscription()
|
|
91
|
+
appRemote?.disconnect()
|
|
92
|
+
appRemote = nil
|
|
93
|
+
clearConnectionAttempt()
|
|
94
|
+
transitionState("disconnected")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
nonisolated func isConnected() -> Bool {
|
|
98
|
+
connectionStateString == "connected"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func getConnectionState() -> String {
|
|
102
|
+
connectionStateString
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func didConnect() {
|
|
106
|
+
clearConnectionAttempt()
|
|
107
|
+
transitionState("connected")
|
|
108
|
+
setupPlayerSubscription()
|
|
109
|
+
setupCapabilitiesSubscription()
|
|
110
|
+
let cont = connectContinuation
|
|
111
|
+
connectContinuation = nil
|
|
112
|
+
cont?.resume()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func didFailToConnect(error: Error?) {
|
|
116
|
+
appRemote = nil
|
|
117
|
+
clearConnectionAttempt()
|
|
118
|
+
let msg = error.map { describeNSError($0 as NSError) } ?? "Unknown connection failure"
|
|
119
|
+
let remoteError = AppRemoteError.connectionFailed(msg)
|
|
120
|
+
transitionState("disconnected")
|
|
121
|
+
onConnectionError?(remoteError.code, remoteError.message)
|
|
122
|
+
let cont = connectContinuation
|
|
123
|
+
connectContinuation = nil
|
|
124
|
+
cont?.resume(throwing: remoteError)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func didDisconnect(error: Error?) {
|
|
128
|
+
appRemote = nil
|
|
129
|
+
transitionState("disconnected")
|
|
130
|
+
if let error = error {
|
|
131
|
+
let msg = describeNSError(error as NSError)
|
|
132
|
+
onConnectionError?(AppRemoteError.connectionLost(msg).code, msg)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// MARK: — Private connection helpers
|
|
137
|
+
|
|
138
|
+
private func beginConnection(
|
|
139
|
+
accessToken: String,
|
|
140
|
+
kickoff: AppRemoteConnectionKickoff,
|
|
141
|
+
) async throws {
|
|
142
|
+
guard connectContinuation == nil else {
|
|
143
|
+
throw AppRemoteError.connectionFailed("A connection attempt is already in progress")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
transitionState("connecting")
|
|
147
|
+
|
|
148
|
+
let remote = makeAppRemote(accessToken: accessToken)
|
|
149
|
+
appRemote = remote
|
|
150
|
+
connectionAttempt = AppRemoteConnectionAttempt(remote: remote, kickoff: kickoff)
|
|
151
|
+
|
|
152
|
+
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
|
153
|
+
connectContinuation = cont
|
|
154
|
+
Task { @MainActor in
|
|
155
|
+
switch kickoff {
|
|
156
|
+
case .directConnect:
|
|
157
|
+
remote.connect()
|
|
158
|
+
case .authorizeAndPlay(let uri):
|
|
159
|
+
let success = await remote.authorizeAndPlayURI(uri)
|
|
160
|
+
if !success {
|
|
161
|
+
await self.didFailToConnect(error: Self.authorizeOpenFailedError())
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private func makeAppRemote(accessToken: String) -> SPTAppRemote {
|
|
169
|
+
let params = SPTAppRemoteConnectionParams(
|
|
170
|
+
accessToken: accessToken,
|
|
171
|
+
defaultImageSize: CGSize.zero,
|
|
172
|
+
imageFormat: .any
|
|
173
|
+
)
|
|
174
|
+
let remote = SPTAppRemote(
|
|
175
|
+
configuration: sptConfiguration,
|
|
176
|
+
connectionParameters: params,
|
|
177
|
+
logLevel: .error
|
|
178
|
+
)
|
|
179
|
+
remote.delegate = connectionBridge
|
|
180
|
+
return remote
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
nonisolated private func clearConnectionAttempt() {
|
|
184
|
+
connectionAttempt = nil
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private static func authorizeOpenFailedError() -> NSError {
|
|
188
|
+
NSError(
|
|
189
|
+
domain: "ExpoSpotifySDK",
|
|
190
|
+
code: -1,
|
|
191
|
+
userInfo: [
|
|
192
|
+
NSLocalizedDescriptionKey:
|
|
193
|
+
"authorizeAndPlay: could not open the Spotify app (is it installed?)",
|
|
194
|
+
]
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// MARK: — Connection delegate bridge
|
|
200
|
+
|
|
201
|
+
/// `SPTAppRemoteDelegate` requires `NSObject` conformance, which an `actor`
|
|
202
|
+
/// cannot satisfy directly. This bridge is a tiny `NSObject` that hops each
|
|
203
|
+
/// delegate callback onto the actor's executor via a `Task`.
|
|
204
|
+
final class SpotifyAppRemoteDelegateBridge: NSObject, SPTAppRemoteDelegate {
|
|
205
|
+
weak var coordinator: SpotifyAppRemoteCoordinator?
|
|
206
|
+
|
|
207
|
+
func appRemoteDidEstablishConnection(_ appRemote: SPTAppRemote) {
|
|
208
|
+
NSLog("[ExpoSpotifySDK] AppRemote: connection established")
|
|
209
|
+
Task { await coordinator?.didConnect() }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
func appRemote(_ appRemote: SPTAppRemote, didFailConnectionAttemptWithError error: (any Error)?) {
|
|
213
|
+
NSLog("[ExpoSpotifySDK] AppRemote: connection failed — %@", String(describing: error))
|
|
214
|
+
Task { await coordinator?.didFailToConnect(error: error) }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
func appRemote(_ appRemote: SPTAppRemote, didDisconnectWithError error: (any Error)?) {
|
|
218
|
+
NSLog("[ExpoSpotifySDK] AppRemote: disconnected — %@", String(describing: error))
|
|
219
|
+
Task { await coordinator?.didDisconnect(error: error) }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -18,12 +18,16 @@ import UIKit
|
|
|
18
18
|
actor SpotifyAppRemoteCoordinator {
|
|
19
19
|
static let shared: SpotifyAppRemoteCoordinator? = SpotifyAppRemoteCoordinator.create()
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
let sptConfiguration: SPTConfiguration
|
|
22
|
+
let connectionBridge: SpotifyAppRemoteDelegateBridge
|
|
23
23
|
private let playerStateBridge: SpotifyPlayerStateDelegateBridge
|
|
24
24
|
private let userCapabilitiesBridge: SpotifyUserCapabilitiesDelegateBridge
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
var appRemote: SPTAppRemote?
|
|
26
|
+
var connectContinuation: CheckedContinuation<Void, Error>?
|
|
27
|
+
|
|
28
|
+
/// In-flight connection attempt, readable from the synchronous redirect path.
|
|
29
|
+
/// `nil` whenever no connection attempt is active.
|
|
30
|
+
nonisolated(unsafe) var connectionAttempt: AppRemoteConnectionAttempt?
|
|
27
31
|
|
|
28
32
|
nonisolated(unsafe) private(set) var connectionStateString: String = "disconnected"
|
|
29
33
|
|
|
@@ -68,95 +72,15 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
68
72
|
return SpotifyAppRemoteCoordinator(sptConfiguration: sptConfig)
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
// MARK: — Connection lifecycle
|
|
72
|
-
|
|
73
|
-
func connect(accessToken: String) async throws {
|
|
74
|
-
if appRemote?.isConnected == true { return }
|
|
75
|
-
guard connectContinuation == nil else {
|
|
76
|
-
throw AppRemoteError.connectionFailed("A connection attempt is already in progress")
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
transitionState("connecting")
|
|
80
|
-
|
|
81
|
-
let params = SPTAppRemoteConnectionParams(
|
|
82
|
-
accessToken: accessToken,
|
|
83
|
-
defaultImageSize: CGSize.zero,
|
|
84
|
-
imageFormat: .any
|
|
85
|
-
)
|
|
86
|
-
let remote = SPTAppRemote(
|
|
87
|
-
configuration: sptConfiguration,
|
|
88
|
-
connectionParameters: params,
|
|
89
|
-
logLevel: .error
|
|
90
|
-
)
|
|
91
|
-
remote.delegate = connectionBridge
|
|
92
|
-
appRemote = remote
|
|
93
|
-
|
|
94
|
-
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
|
|
95
|
-
self.connectContinuation = cont
|
|
96
|
-
Task { @MainActor in remote.connect() }
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
func disconnect() {
|
|
101
|
-
if let cont = connectContinuation {
|
|
102
|
-
connectContinuation = nil
|
|
103
|
-
cont.resume(throwing: AppRemoteError.connectionFailed("Disconnected before connection completed"))
|
|
104
|
-
}
|
|
105
|
-
teardownPlayerSubscription()
|
|
106
|
-
teardownCapabilitiesSubscription()
|
|
107
|
-
appRemote?.disconnect()
|
|
108
|
-
appRemote = nil
|
|
109
|
-
transitionState("disconnected")
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
nonisolated func isConnected() -> Bool {
|
|
113
|
-
connectionStateString == "connected"
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
func getConnectionState() -> String {
|
|
117
|
-
connectionStateString
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// MARK: — Connection delegate callbacks
|
|
121
|
-
|
|
122
|
-
func didConnect() {
|
|
123
|
-
transitionState("connected")
|
|
124
|
-
setupPlayerSubscription()
|
|
125
|
-
setupCapabilitiesSubscription()
|
|
126
|
-
let cont = connectContinuation
|
|
127
|
-
connectContinuation = nil
|
|
128
|
-
cont?.resume()
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
func didFailToConnect(error: Error?) {
|
|
132
|
-
appRemote = nil
|
|
133
|
-
let msg = error.map { describeNSError($0 as NSError) } ?? "Unknown connection failure"
|
|
134
|
-
let remoteError = AppRemoteError.connectionFailed(msg)
|
|
135
|
-
transitionState("disconnected")
|
|
136
|
-
onConnectionError?(remoteError.code, remoteError.message)
|
|
137
|
-
let cont = connectContinuation
|
|
138
|
-
connectContinuation = nil
|
|
139
|
-
cont?.resume(throwing: remoteError)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
func didDisconnect(error: Error?) {
|
|
143
|
-
appRemote = nil
|
|
144
|
-
transitionState("disconnected")
|
|
145
|
-
if let error = error {
|
|
146
|
-
let msg = describeNSError(error as NSError)
|
|
147
|
-
onConnectionError?(AppRemoteError.connectionLost(msg).code, msg)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
75
|
// MARK: — Player subscription
|
|
152
76
|
|
|
153
|
-
|
|
77
|
+
func setupPlayerSubscription() {
|
|
154
78
|
guard let playerAPI = appRemote?.playerAPI else { return }
|
|
155
79
|
playerAPI.delegate = playerStateBridge
|
|
156
80
|
playerAPI.subscribe(toPlayerState: { _, _ in })
|
|
157
81
|
}
|
|
158
82
|
|
|
159
|
-
|
|
83
|
+
func teardownPlayerSubscription() {
|
|
160
84
|
guard let playerAPI = appRemote?.playerAPI else { return }
|
|
161
85
|
playerAPI.delegate = nil
|
|
162
86
|
playerAPI.unsubscribe(toPlayerState: { _, _ in })
|
|
@@ -169,13 +93,13 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
169
93
|
|
|
170
94
|
// MARK: — User subscription
|
|
171
95
|
|
|
172
|
-
|
|
96
|
+
func setupCapabilitiesSubscription() {
|
|
173
97
|
guard let userAPI = appRemote?.userAPI else { return }
|
|
174
98
|
userAPI.delegate = userCapabilitiesBridge
|
|
175
99
|
userAPI.subscribe(toCapabilityChanges: { _, _ in })
|
|
176
100
|
}
|
|
177
101
|
|
|
178
|
-
|
|
102
|
+
func teardownCapabilitiesSubscription() {
|
|
179
103
|
guard let userAPI = appRemote?.userAPI else { return }
|
|
180
104
|
userAPI.delegate = nil
|
|
181
105
|
userAPI.unsubscribe(toCapabilityChanges: { _, _ in })
|
|
@@ -490,12 +414,12 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
490
414
|
}
|
|
491
415
|
}
|
|
492
416
|
|
|
493
|
-
|
|
417
|
+
func transitionState(_ state: String) {
|
|
494
418
|
connectionStateString = state
|
|
495
419
|
onConnectionStateChange?(state)
|
|
496
420
|
}
|
|
497
421
|
|
|
498
|
-
|
|
422
|
+
func describeNSError(_ error: NSError) -> String {
|
|
499
423
|
var parts: [String] = ["\(error.domain) code \(error.code)"]
|
|
500
424
|
let desc = error.localizedDescription
|
|
501
425
|
if !desc.isEmpty { parts.append("\"\(desc)\"") }
|
|
@@ -506,30 +430,6 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
506
430
|
}
|
|
507
431
|
}
|
|
508
432
|
|
|
509
|
-
// MARK: — Connection delegate bridge
|
|
510
|
-
|
|
511
|
-
/// `SPTAppRemoteDelegate` requires `NSObject` conformance, which an `actor`
|
|
512
|
-
/// cannot satisfy directly. This bridge is a tiny `NSObject` that hops each
|
|
513
|
-
/// delegate callback onto the actor's executor via a `Task`.
|
|
514
|
-
final class SpotifyAppRemoteDelegateBridge: NSObject, SPTAppRemoteDelegate {
|
|
515
|
-
weak var coordinator: SpotifyAppRemoteCoordinator?
|
|
516
|
-
|
|
517
|
-
func appRemoteDidEstablishConnection(_ appRemote: SPTAppRemote) {
|
|
518
|
-
NSLog("[ExpoSpotifySDK] AppRemote: connection established")
|
|
519
|
-
Task { await coordinator?.didConnect() }
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
func appRemote(_ appRemote: SPTAppRemote, didFailConnectionAttemptWithError error: (any Error)?) {
|
|
523
|
-
NSLog("[ExpoSpotifySDK] AppRemote: connection failed — %@", String(describing: error))
|
|
524
|
-
Task { await coordinator?.didFailToConnect(error: error) }
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
func appRemote(_ appRemote: SPTAppRemote, didDisconnectWithError error: (any Error)?) {
|
|
528
|
-
NSLog("[ExpoSpotifySDK] AppRemote: disconnected — %@", String(describing: error))
|
|
529
|
-
Task { await coordinator?.didDisconnect(error: error) }
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
433
|
// MARK: — Player state delegate bridge
|
|
534
434
|
|
|
535
435
|
/// `SPTAppRemotePlayerStateDelegate` requires `NSObject` conformance. This
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
2
|
import Foundation
|
|
3
3
|
import SpotifyiOS
|
|
4
|
+
import UIKit
|
|
4
5
|
|
|
5
6
|
/// Public, structured errors thrown by the coordinator. Each case carries the
|
|
6
7
|
/// JS-facing error code in `code`, mirroring the Android `CodedException`
|
|
@@ -105,15 +106,9 @@ actor SpotifyAuthCoordinator {
|
|
|
105
106
|
/// Synchronous URL-handler dispatcher used by the AppDelegate / SceneDelegate
|
|
106
107
|
/// subscribers to forward redirects to the SPTSessionManager.
|
|
107
108
|
nonisolated func handleOpenURL(_ url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
sessionManager.application(UIApplication.shared, open: url, options: options)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if Thread.isMainThread {
|
|
114
|
-
return openSession()
|
|
109
|
+
SpotifyMainThread.run {
|
|
110
|
+
sessionManager.application(UIApplication.shared, open: url, options: options)
|
|
115
111
|
}
|
|
116
|
-
return DispatchQueue.main.sync(execute: openSession)
|
|
117
112
|
}
|
|
118
113
|
|
|
119
114
|
func authenticate(
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Runs a block on the main thread, matching AppDelegate / SceneDelegate redirect
|
|
4
|
+
/// hooks that require a synchronous result from the main actor.
|
|
5
|
+
enum SpotifyMainThread {
|
|
6
|
+
static func run<T>(_ block: @MainActor () -> T) -> T {
|
|
7
|
+
if Thread.isMainThread {
|
|
8
|
+
return MainActor.assumeIsolated(block)
|
|
9
|
+
}
|
|
10
|
+
return DispatchQueue.main.sync {
|
|
11
|
+
MainActor.assumeIsolated(block)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ios": {
|
|
3
|
+
"spmRepositoryUrl": "https://github.com/spotify/ios-sdk.git",
|
|
4
|
+
"spmVersion": "5.0.1",
|
|
5
|
+
"spmProduct": "SpotifyiOS"
|
|
6
|
+
},
|
|
7
|
+
"android": {
|
|
8
|
+
"appRemoteVersion": "0.8.0",
|
|
9
|
+
"appRemoteReleaseTag": "v0.8.0-appremote_v2.1.0-auth",
|
|
10
|
+
"appRemoteSha256": "b5a6dd880eaf01f63a871cba9ef7af77c341f8a94ffc8fdf2e9021f9a9d4c198"
|
|
11
|
+
}
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wwdrew/expo-spotify-sdk",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Expo module wrapping the native Spotify iOS (v5) and Android (v4) SDKs for OAuth authentication and App Remote playback control",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "tsc",
|
|
19
19
|
"build:plugin": "tsc -p plugin/tsconfig.json",
|
|
20
|
-
"clean": "rm -rf build plugin/build",
|
|
20
|
+
"clean": "rm -rf build plugin/build android/build",
|
|
21
21
|
"lint": "eslint src",
|
|
22
22
|
"test": "jest --config jest.config.js",
|
|
23
23
|
"prepublishOnly": "yarn clean && yarn build && yarn build:plugin",
|
|
@@ -37,8 +37,14 @@
|
|
|
37
37
|
"plugin/index.d.ts",
|
|
38
38
|
"plugin/build",
|
|
39
39
|
"build",
|
|
40
|
-
"android",
|
|
41
|
-
"
|
|
40
|
+
"android/build.gradle",
|
|
41
|
+
"android/spotify-native-sdk.gradle",
|
|
42
|
+
"android/consumer-rules.pro",
|
|
43
|
+
"android/src",
|
|
44
|
+
"android/libs/SETUP.md",
|
|
45
|
+
"ios/spotify-native-sdk-versions.json",
|
|
46
|
+
"ios/*.swift",
|
|
47
|
+
"ios/*.podspec",
|
|
42
48
|
"app.plugin.js",
|
|
43
49
|
"expo-module.config.json"
|
|
44
50
|
],
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
The MIT License (MIT)
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2014 Gabriel Handford
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
-
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
-
the Software without restriction, including without limitation the rights to
|
|
8
|
-
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
-
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
-
subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
-
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
-
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
-
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
-
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>AvailableLibraries</key>
|
|
6
|
-
<array>
|
|
7
|
-
<dict>
|
|
8
|
-
<key>LibraryIdentifier</key>
|
|
9
|
-
<string>ios-arm64</string>
|
|
10
|
-
<key>LibraryPath</key>
|
|
11
|
-
<string>SpotifyiOS.framework</string>
|
|
12
|
-
<key>SupportedArchitectures</key>
|
|
13
|
-
<array>
|
|
14
|
-
<string>arm64</string>
|
|
15
|
-
</array>
|
|
16
|
-
<key>SupportedPlatform</key>
|
|
17
|
-
<string>ios</string>
|
|
18
|
-
</dict>
|
|
19
|
-
<dict>
|
|
20
|
-
<key>LibraryIdentifier</key>
|
|
21
|
-
<string>ios-arm64_x86_64-simulator</string>
|
|
22
|
-
<key>LibraryPath</key>
|
|
23
|
-
<string>SpotifyiOS.framework</string>
|
|
24
|
-
<key>SupportedArchitectures</key>
|
|
25
|
-
<array>
|
|
26
|
-
<string>arm64</string>
|
|
27
|
-
<string>x86_64</string>
|
|
28
|
-
</array>
|
|
29
|
-
<key>SupportedPlatform</key>
|
|
30
|
-
<string>ios</string>
|
|
31
|
-
<key>SupportedPlatformVariant</key>
|
|
32
|
-
<string>simulator</string>
|
|
33
|
-
</dict>
|
|
34
|
-
</array>
|
|
35
|
-
<key>CFBundlePackageType</key>
|
|
36
|
-
<string>XFWK</string>
|
|
37
|
-
<key>XCFrameworkFormatVersion</key>
|
|
38
|
-
<string>1.0</string>
|
|
39
|
-
</dict>
|
|
40
|
-
</plist>
|