@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.
Files changed (89) hide show
  1. package/README.md +32 -2
  2. package/android/build.gradle +4 -5
  3. package/android/libs/SETUP.md +9 -24
  4. package/android/spotify-native-sdk.gradle +116 -0
  5. package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +12 -1
  6. package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteCoordinator.kt +32 -0
  7. package/build/app-remote/index.d.ts +9 -0
  8. package/build/app-remote/index.d.ts.map +1 -1
  9. package/build/app-remote/index.js +11 -0
  10. package/build/app-remote/index.js.map +1 -1
  11. package/ios/ExpoSpotifyAppDelegate.swift +8 -1
  12. package/ios/ExpoSpotifySDK.podspec +26 -2
  13. package/ios/ExpoSpotifySDKModule.swift +16 -1
  14. package/ios/SpotifyAppRemoteConnection.swift +221 -0
  15. package/ios/SpotifyAppRemoteCoordinator.swift +14 -114
  16. package/ios/SpotifyAuthCoordinator.swift +3 -8
  17. package/ios/SpotifyMainThread.swift +14 -0
  18. package/ios/spotify-native-sdk-versions.json +12 -0
  19. package/package.json +10 -4
  20. package/ios/SpotifySDK/Licenses/MPMessagePack-LICENSE.md +0 -20
  21. package/ios/SpotifySDK/SpotifyiOS.xcframework/Info.plist +0 -40
  22. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemote.h +0 -287
  23. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteAlbum.h +0 -18
  24. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteArtist.h +0 -18
  25. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteCommon.h +0 -37
  26. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectionParams.h +0 -65
  27. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityAPI.h +0 -55
  28. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityState.h +0 -15
  29. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteContentAPI.h +0 -78
  30. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteContentItem.h +0 -49
  31. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteCrossfadeState.h +0 -18
  32. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteImageAPI.h +0 -25
  33. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteImageRepresentable.h +0 -16
  34. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteLibraryState.h +0 -19
  35. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackOptions.h +0 -32
  36. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackRestrictions.h +0 -32
  37. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlayerAPI.h +0 -267
  38. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlayerState.h +0 -40
  39. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePodcastPlaybackSpeed.h +0 -15
  40. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteTrack.h +0 -52
  41. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteUserAPI.h +0 -102
  42. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteUserCapabilities.h +0 -15
  43. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTConfiguration.h +0 -66
  44. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTError.h +0 -31
  45. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTLogin.h +0 -10
  46. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTMacros.h +0 -125
  47. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTScope.h +0 -75
  48. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTSession.h +0 -33
  49. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTSessionManager.h +0 -170
  50. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SpotifyAppRemote.h +0 -28
  51. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SpotifyiOS.h +0 -7
  52. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Info.plist +0 -0
  53. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Modules/module.modulemap +0 -5
  54. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/PrivacyInfo.xcprivacy +0 -14
  55. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/SpotifyiOS +0 -0
  56. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemote.h +0 -287
  57. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteAlbum.h +0 -18
  58. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteArtist.h +0 -18
  59. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteCommon.h +0 -37
  60. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectionParams.h +0 -65
  61. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityAPI.h +0 -55
  62. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityState.h +0 -15
  63. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteContentAPI.h +0 -78
  64. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteContentItem.h +0 -49
  65. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteCrossfadeState.h +0 -18
  66. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteImageAPI.h +0 -25
  67. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteImageRepresentable.h +0 -16
  68. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteLibraryState.h +0 -19
  69. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackOptions.h +0 -32
  70. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackRestrictions.h +0 -32
  71. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlayerAPI.h +0 -267
  72. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlayerState.h +0 -40
  73. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePodcastPlaybackSpeed.h +0 -15
  74. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteTrack.h +0 -52
  75. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteUserAPI.h +0 -102
  76. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteUserCapabilities.h +0 -15
  77. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTConfiguration.h +0 -66
  78. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTError.h +0 -31
  79. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTLogin.h +0 -10
  80. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTMacros.h +0 -125
  81. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTScope.h +0 -75
  82. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTSession.h +0 -33
  83. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTSessionManager.h +0 -170
  84. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SpotifyAppRemote.h +0 -28
  85. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SpotifyiOS.h +0 -7
  86. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Info.plist +0 -0
  87. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Modules/module.modulemap +0 -5
  88. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/PrivacyInfo.xcprivacy +0 -14
  89. 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
- private let sptConfiguration: SPTConfiguration
22
- private let connectionBridge: SpotifyAppRemoteDelegateBridge
21
+ let sptConfiguration: SPTConfiguration
22
+ let connectionBridge: SpotifyAppRemoteDelegateBridge
23
23
  private let playerStateBridge: SpotifyPlayerStateDelegateBridge
24
24
  private let userCapabilitiesBridge: SpotifyUserCapabilitiesDelegateBridge
25
- private var appRemote: SPTAppRemote?
26
- private var connectContinuation: CheckedContinuation<Void, Error>?
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
- private func setupPlayerSubscription() {
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
- private func teardownPlayerSubscription() {
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
- private func setupCapabilitiesSubscription() {
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
- private func teardownCapabilitiesSubscription() {
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
- private func transitionState(_ state: String) {
417
+ func transitionState(_ state: String) {
494
418
  connectionStateString = state
495
419
  onConnectionStateChange?(state)
496
420
  }
497
421
 
498
- private func describeNSError(_ error: NSError) -> String {
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
- let openSession: () -> Bool = { [self] in
109
- MainActor.assumeIsolated {
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.0",
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
- "ios",
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>