@wwdrew/expo-spotify-sdk 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +94 -346
  2. package/android/build.gradle +4 -5
  3. package/android/libs/SETUP.md +9 -24
  4. package/android/spotify-native-sdk.gradle +118 -0
  5. package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +14 -5
  6. package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteCoordinator.kt +38 -64
  7. package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteErrorMapping.kt +106 -0
  8. package/android/src/main/java/expo/modules/spotifysdk/SpotifyTokenSwapClient.kt +2 -7
  9. package/build/app-remote/index.d.ts +34 -0
  10. package/build/app-remote/index.d.ts.map +1 -1
  11. package/build/app-remote/index.js +47 -24
  12. package/build/app-remote/index.js.map +1 -1
  13. package/build/auth/index.d.ts.map +1 -1
  14. package/build/auth/index.js +17 -34
  15. package/build/auth/index.js.map +1 -1
  16. package/build/content/index.d.ts.map +1 -1
  17. package/build/content/index.js +12 -25
  18. package/build/content/index.js.map +1 -1
  19. package/build/hooks/index.d.ts +1 -1
  20. package/build/hooks/index.d.ts.map +1 -1
  21. package/build/hooks/index.js +38 -115
  22. package/build/hooks/index.js.map +1 -1
  23. package/build/images/index.d.ts +2 -2
  24. package/build/images/index.d.ts.map +1 -1
  25. package/build/images/index.js +12 -25
  26. package/build/images/index.js.map +1 -1
  27. package/build/index.d.ts +0 -18
  28. package/build/index.d.ts.map +1 -1
  29. package/build/index.js +0 -27
  30. package/build/index.js.map +1 -1
  31. package/build/internal/native-errors.d.ts +14 -0
  32. package/build/internal/native-errors.d.ts.map +1 -0
  33. package/build/internal/native-errors.js +29 -0
  34. package/build/internal/native-errors.js.map +1 -0
  35. package/build/internal/sync-external-store.d.ts +14 -0
  36. package/build/internal/sync-external-store.d.ts.map +1 -0
  37. package/build/internal/sync-external-store.js +36 -0
  38. package/build/internal/sync-external-store.js.map +1 -0
  39. package/build/player/index.d.ts.map +1 -1
  40. package/build/player/index.js +14 -27
  41. package/build/player/index.js.map +1 -1
  42. package/build/user/index.d.ts +2 -2
  43. package/build/user/index.d.ts.map +1 -1
  44. package/build/user/index.js +16 -26
  45. package/build/user/index.js.map +1 -1
  46. package/ios/ExpoSpotifyAppDelegate.swift +8 -1
  47. package/ios/ExpoSpotifySDK.podspec +26 -2
  48. package/ios/ExpoSpotifySDKModule.swift +16 -1
  49. package/ios/SpotifyAppRemoteCoordinator.swift +137 -404
  50. package/ios/SpotifyAppRemoteErrorMapping.swift +110 -0
  51. package/ios/SpotifyAppRemoteErrors.swift +221 -0
  52. package/ios/SpotifyAppRemoteMappers.swift +88 -0
  53. package/ios/spotify-native-sdk-versions.json +12 -0
  54. package/package.json +13 -5
  55. package/plugin/index.d.ts +2 -0
  56. package/plugin/index.js +1 -0
  57. package/ios/SpotifySDK/Licenses/MPMessagePack-LICENSE.md +0 -20
  58. package/ios/SpotifySDK/SpotifyiOS.xcframework/Info.plist +0 -40
  59. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemote.h +0 -287
  60. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteAlbum.h +0 -18
  61. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteArtist.h +0 -18
  62. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteCommon.h +0 -37
  63. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectionParams.h +0 -65
  64. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityAPI.h +0 -55
  65. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityState.h +0 -15
  66. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteContentAPI.h +0 -78
  67. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteContentItem.h +0 -49
  68. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteCrossfadeState.h +0 -18
  69. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteImageAPI.h +0 -25
  70. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteImageRepresentable.h +0 -16
  71. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteLibraryState.h +0 -19
  72. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackOptions.h +0 -32
  73. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackRestrictions.h +0 -32
  74. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlayerAPI.h +0 -267
  75. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlayerState.h +0 -40
  76. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePodcastPlaybackSpeed.h +0 -15
  77. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteTrack.h +0 -52
  78. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteUserAPI.h +0 -102
  79. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteUserCapabilities.h +0 -15
  80. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTConfiguration.h +0 -66
  81. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTError.h +0 -31
  82. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTLogin.h +0 -10
  83. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTMacros.h +0 -125
  84. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTScope.h +0 -75
  85. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTSession.h +0 -33
  86. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTSessionManager.h +0 -170
  87. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SpotifyAppRemote.h +0 -28
  88. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SpotifyiOS.h +0 -7
  89. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Info.plist +0 -0
  90. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Modules/module.modulemap +0 -5
  91. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/PrivacyInfo.xcprivacy +0 -14
  92. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/SpotifyiOS +0 -0
  93. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemote.h +0 -287
  94. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteAlbum.h +0 -18
  95. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteArtist.h +0 -18
  96. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteCommon.h +0 -37
  97. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectionParams.h +0 -65
  98. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityAPI.h +0 -55
  99. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityState.h +0 -15
  100. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteContentAPI.h +0 -78
  101. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteContentItem.h +0 -49
  102. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteCrossfadeState.h +0 -18
  103. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteImageAPI.h +0 -25
  104. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteImageRepresentable.h +0 -16
  105. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteLibraryState.h +0 -19
  106. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackOptions.h +0 -32
  107. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackRestrictions.h +0 -32
  108. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlayerAPI.h +0 -267
  109. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlayerState.h +0 -40
  110. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePodcastPlaybackSpeed.h +0 -15
  111. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteTrack.h +0 -52
  112. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteUserAPI.h +0 -102
  113. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteUserCapabilities.h +0 -15
  114. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTConfiguration.h +0 -66
  115. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTError.h +0 -31
  116. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTLogin.h +0 -10
  117. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTMacros.h +0 -125
  118. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTScope.h +0 -75
  119. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTSession.h +0 -33
  120. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTSessionManager.h +0 -170
  121. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SpotifyAppRemote.h +0 -28
  122. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SpotifyiOS.h +0 -7
  123. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Info.plist +0 -0
  124. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Modules/module.modulemap +0 -5
  125. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/PrivacyInfo.xcprivacy +0 -14
  126. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/SpotifyiOS +0 -0
@@ -1,227 +1,7 @@
1
- import ExpoModulesCore
2
1
  import Foundation
3
2
  import SpotifyiOS
4
3
  import UIKit
5
4
 
6
- // MARK: — App Remote error types
7
-
8
- enum AppRemoteError: Error {
9
- case connectionFailed(String)
10
- case connectionLost(String)
11
- case notConnected(String)
12
- case unknown(String)
13
-
14
- var code: String {
15
- switch self {
16
- case .connectionFailed: return "CONNECTION_FAILED"
17
- case .connectionLost: return "CONNECTION_LOST"
18
- case .notConnected: return "NOT_CONNECTED"
19
- case .unknown: return "UNKNOWN"
20
- }
21
- }
22
-
23
- var message: String {
24
- switch self {
25
- case .connectionFailed(let m): return m
26
- case .connectionLost(let m): return m
27
- case .notConnected(let m): return m
28
- case .unknown(let m): return m
29
- }
30
- }
31
- }
32
-
33
- /// Bridges an `AppRemoteError` through expo-modules-core's exception system so
34
- /// JS callers receive a structured `code` and `reason`, not "undefined reason".
35
- final class AppRemoteException: Exception, @unchecked Sendable {
36
- private let appRemoteCode: String
37
- private let appRemoteMessage: String
38
-
39
- init(_ error: AppRemoteError, file: String = #fileID, line: UInt = #line, function: String = #function) {
40
- self.appRemoteCode = error.code
41
- self.appRemoteMessage = error.message
42
- super.init(file: file, line: line, function: function)
43
- }
44
-
45
- override var code: String { appRemoteCode }
46
- override var reason: String { appRemoteMessage }
47
- }
48
-
49
- // MARK: — Player error types
50
-
51
- enum NativePlayerError: Error {
52
- case notConnected(String)
53
- case connectionLost(String)
54
- case premiumRequired(String)
55
- case invalidURI(String)
56
- case invalidParameter(String)
57
- case operationNotAllowed(String)
58
- case unknown(String)
59
-
60
- var code: String {
61
- switch self {
62
- case .notConnected: return "NOT_CONNECTED"
63
- case .connectionLost: return "CONNECTION_LOST"
64
- case .premiumRequired: return "PREMIUM_REQUIRED"
65
- case .invalidURI: return "INVALID_URI"
66
- case .invalidParameter: return "INVALID_PARAMETER"
67
- case .operationNotAllowed: return "OPERATION_NOT_ALLOWED"
68
- case .unknown: return "UNKNOWN"
69
- }
70
- }
71
-
72
- var message: String {
73
- switch self {
74
- case .notConnected(let m): return m
75
- case .connectionLost(let m): return m
76
- case .premiumRequired(let m): return m
77
- case .invalidURI(let m): return m
78
- case .invalidParameter(let m): return m
79
- case .operationNotAllowed(let m): return m
80
- case .unknown(let m): return m
81
- }
82
- }
83
- }
84
-
85
- final class PlayerException: Exception, @unchecked Sendable {
86
- private let playerCode: String
87
- private let playerMessage: String
88
-
89
- init(_ error: NativePlayerError, file: String = #fileID, line: UInt = #line, function: String = #function) {
90
- self.playerCode = error.code
91
- self.playerMessage = error.message
92
- super.init(file: file, line: line, function: function)
93
- }
94
-
95
- override var code: String { playerCode }
96
- override var reason: String { playerMessage }
97
- }
98
-
99
- // MARK: — User error types
100
-
101
- enum NativeUserError: Error {
102
- case notConnected(String)
103
- case connectionLost(String)
104
- case invalidURI(String)
105
- case operationNotAllowed(String)
106
- case unknown(String)
107
-
108
- var code: String {
109
- switch self {
110
- case .notConnected: return "NOT_CONNECTED"
111
- case .connectionLost: return "CONNECTION_LOST"
112
- case .invalidURI: return "INVALID_URI"
113
- case .operationNotAllowed: return "OPERATION_NOT_ALLOWED"
114
- case .unknown: return "UNKNOWN"
115
- }
116
- }
117
-
118
- var message: String {
119
- switch self {
120
- case .notConnected(let m): return m
121
- case .connectionLost(let m): return m
122
- case .invalidURI(let m): return m
123
- case .operationNotAllowed(let m): return m
124
- case .unknown(let m): return m
125
- }
126
- }
127
- }
128
-
129
- final class UserException: Exception, @unchecked Sendable {
130
- private let userCode: String
131
- private let userMessage: String
132
-
133
- init(_ error: NativeUserError, file: String = #fileID, line: UInt = #line, function: String = #function) {
134
- self.userCode = error.code
135
- self.userMessage = error.message
136
- super.init(file: file, line: line, function: function)
137
- }
138
-
139
- override var code: String { userCode }
140
- override var reason: String { userMessage }
141
- }
142
-
143
- // MARK: — Content error types
144
-
145
- enum NativeContentError: Error {
146
- case notConnected(String)
147
- case connectionLost(String)
148
- case contentAPIUnavailable(String)
149
- case unknown(String)
150
-
151
- var code: String {
152
- switch self {
153
- case .notConnected: return "NOT_CONNECTED"
154
- case .connectionLost: return "CONNECTION_LOST"
155
- case .contentAPIUnavailable: return "CONTENT_API_UNAVAILABLE"
156
- case .unknown: return "UNKNOWN"
157
- }
158
- }
159
-
160
- var message: String {
161
- switch self {
162
- case .notConnected(let m): return m
163
- case .connectionLost(let m): return m
164
- case .contentAPIUnavailable(let m): return m
165
- case .unknown(let m): return m
166
- }
167
- }
168
- }
169
-
170
- final class ContentException: Exception, @unchecked Sendable {
171
- private let contentCode: String
172
- private let contentMessage: String
173
-
174
- init(_ error: NativeContentError, file: String = #fileID, line: UInt = #line, function: String = #function) {
175
- self.contentCode = error.code
176
- self.contentMessage = error.message
177
- super.init(file: file, line: line, function: function)
178
- }
179
-
180
- override var code: String { contentCode }
181
- override var reason: String { contentMessage }
182
- }
183
-
184
- // MARK: — Images error types
185
-
186
- enum NativeImagesError: Error {
187
- case notConnected(String)
188
- case invalidURI(String)
189
- case imageLoadFailed(String)
190
- case unknown(String)
191
-
192
- var code: String {
193
- switch self {
194
- case .notConnected: return "NOT_CONNECTED"
195
- case .invalidURI: return "INVALID_URI"
196
- case .imageLoadFailed: return "IMAGE_LOAD_FAILED"
197
- case .unknown: return "UNKNOWN"
198
- }
199
- }
200
-
201
- var message: String {
202
- switch self {
203
- case .notConnected(let m): return m
204
- case .invalidURI(let m): return m
205
- case .imageLoadFailed(let m): return m
206
- case .unknown(let m): return m
207
- }
208
- }
209
- }
210
-
211
- final class ImagesException: Exception, @unchecked Sendable {
212
- private let imagesCode: String
213
- private let imagesMessage: String
214
-
215
- init(_ error: NativeImagesError, file: String = #fileID, line: UInt = #line, function: String = #function) {
216
- self.imagesCode = error.code
217
- self.imagesMessage = error.message
218
- super.init(file: file, line: line, function: function)
219
- }
220
-
221
- override var code: String { imagesCode }
222
- override var reason: String { imagesMessage }
223
- }
224
-
225
5
  // MARK: — Coordinator
226
6
 
227
7
  /// Manages the `SPTAppRemote` singleton, the IPC connection lifecycle, and all
@@ -245,6 +25,14 @@ actor SpotifyAppRemoteCoordinator {
245
25
  private var appRemote: SPTAppRemote?
246
26
  private var connectContinuation: CheckedContinuation<Void, Error>?
247
27
 
28
+ /// The `SPTAppRemote` awaiting an `authorizeAndPlay()` redirect, exposed
29
+ /// synchronously to the URL-redirect path. Mirrors the auth coordinator's
30
+ /// `nonisolated(unsafe)` `sessionManager`: it is only written from the
31
+ /// actor's executor (or the main-thread redirect handler) and read on the
32
+ /// main thread, so occasional cross-actor access is acceptable. `nil`
33
+ /// whenever no authorize-and-play flow is in flight.
34
+ nonisolated(unsafe) private var pendingAuthorizeRemote: SPTAppRemote?
35
+
248
36
  nonisolated(unsafe) private(set) var connectionStateString: String = "disconnected"
249
37
 
250
38
  var onConnectionStateChange: ((String) -> Void)?
@@ -266,9 +54,12 @@ actor SpotifyAppRemoteCoordinator {
266
54
 
267
55
  private init(sptConfiguration: SPTConfiguration) {
268
56
  self.sptConfiguration = sptConfiguration
269
- self.connectionBridge = SpotifyAppRemoteDelegateBridge()
270
- self.playerStateBridge = SpotifyPlayerStateDelegateBridge()
271
- self.userCapabilitiesBridge = SpotifyUserCapabilitiesDelegateBridge()
57
+ let connectionBridge = SpotifyAppRemoteDelegateBridge()
58
+ let playerStateBridge = SpotifyPlayerStateDelegateBridge()
59
+ let userCapabilitiesBridge = SpotifyUserCapabilitiesDelegateBridge()
60
+ self.connectionBridge = connectionBridge
61
+ self.playerStateBridge = playerStateBridge
62
+ self.userCapabilitiesBridge = userCapabilitiesBridge
272
63
  connectionBridge.coordinator = self
273
64
  playerStateBridge.coordinator = self
274
65
  userCapabilitiesBridge.coordinator = self
@@ -314,6 +105,103 @@ actor SpotifyAppRemoteCoordinator {
314
105
  }
315
106
  }
316
107
 
108
+ /// Wakes the Spotify app (via `authorizeAndPlayURI`), starts playback, and
109
+ /// then completes the App Remote connection once Spotify redirects back.
110
+ ///
111
+ /// Unlike `connect()`, this works even when the Spotify app has been
112
+ /// suspended: `authorizeAndPlayURI` performs an app-switch to Spotify, which
113
+ /// revives it. Spotify then redirects back to the host app with an access
114
+ /// token; `handleAuthorizeRedirect(_:)` consumes that redirect and calls
115
+ /// `connect()` on the same `SPTAppRemote` instance.
116
+ ///
117
+ /// The `connectContinuation` is reused for the whole flow, so `didConnect()`
118
+ /// / `didFailToConnect(error:)` resolve it exactly as for `connect()`.
119
+ func authorizeAndPlay(uri: String, accessToken: String) async throws {
120
+ if appRemote?.isConnected == true {
121
+ // Already connected — honour the request by (re)starting playback.
122
+ if uri.isEmpty {
123
+ try await playerResume()
124
+ } else {
125
+ try await playerPlay(uri: uri)
126
+ }
127
+ return
128
+ }
129
+ guard connectContinuation == nil else {
130
+ throw AppRemoteError.connectionFailed("A connection attempt is already in progress")
131
+ }
132
+
133
+ transitionState("connecting")
134
+
135
+ let params = SPTAppRemoteConnectionParams(
136
+ accessToken: accessToken,
137
+ defaultImageSize: CGSize.zero,
138
+ imageFormat: .any
139
+ )
140
+ let remote = SPTAppRemote(
141
+ configuration: sptConfiguration,
142
+ connectionParameters: params,
143
+ logLevel: .error
144
+ )
145
+ remote.delegate = connectionBridge
146
+ appRemote = remote
147
+ pendingAuthorizeRemote = remote
148
+
149
+ try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
150
+ self.connectContinuation = cont
151
+ Task { @MainActor in
152
+ // `authorizeAndPlayURI` app-switches to Spotify to wake it. `success`
153
+ // is false when the Spotify app could not be opened (e.g. not
154
+ // installed) — no redirect will arrive, so fail the continuation now.
155
+ // On success we await the redirect, handled by
156
+ // `handleAuthorizeRedirect(_:)`, which completes the connection.
157
+ let success = await remote.authorizeAndPlayURI(uri)
158
+ if !success {
159
+ await self.didFailToConnect(error: NSError(
160
+ domain: "ExpoSpotifySDK",
161
+ code: -1,
162
+ userInfo: [NSLocalizedDescriptionKey: "authorizeAndPlay: could not open the Spotify app (is it installed?)"]
163
+ ))
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ /// Consumes a redirect URL produced by an `authorizeAndPlay()` app-switch.
170
+ /// Returns `true` if an authorize flow was pending and the URL was handled
171
+ /// (so the AppDelegate should not also forward it to the auth coordinator).
172
+ ///
173
+ /// Synchronous + `nonisolated` because the AppDelegate `open url` hook
174
+ /// expects an immediate `Bool`, matching `SpotifyAuthCoordinator.handleOpenURL`.
175
+ nonisolated func handleAuthorizeRedirect(_ url: URL) -> Bool {
176
+ guard let remote = pendingAuthorizeRemote else { return false }
177
+ let consume: () -> Bool = { [self] in
178
+ MainActor.assumeIsolated {
179
+ let params = remote.authorizationParameters(from: url)
180
+ if let token = params?[SPTAppRemoteAccessTokenKey] {
181
+ remote.connectionParameters.accessToken = token
182
+ pendingAuthorizeRemote = nil
183
+ remote.connect()
184
+ return true
185
+ } else if let errorDescription = params?[SPTAppRemoteErrorDescriptionKey] {
186
+ pendingAuthorizeRemote = nil
187
+ Task {
188
+ await self.didFailToConnect(error: NSError(
189
+ domain: "ExpoSpotifySDK",
190
+ code: -2,
191
+ userInfo: [NSLocalizedDescriptionKey: errorDescription]
192
+ ))
193
+ }
194
+ return true
195
+ }
196
+ return false
197
+ }
198
+ }
199
+ if Thread.isMainThread {
200
+ return consume()
201
+ }
202
+ return DispatchQueue.main.sync(execute: consume)
203
+ }
204
+
317
205
  func disconnect() {
318
206
  if let cont = connectContinuation {
319
207
  connectContinuation = nil
@@ -323,6 +211,7 @@ actor SpotifyAppRemoteCoordinator {
323
211
  teardownCapabilitiesSubscription()
324
212
  appRemote?.disconnect()
325
213
  appRemote = nil
214
+ pendingAuthorizeRemote = nil
326
215
  transitionState("disconnected")
327
216
  }
328
217
 
@@ -337,6 +226,7 @@ actor SpotifyAppRemoteCoordinator {
337
226
  // MARK: — Connection delegate callbacks
338
227
 
339
228
  func didConnect() {
229
+ pendingAuthorizeRemote = nil
340
230
  transitionState("connected")
341
231
  setupPlayerSubscription()
342
232
  setupCapabilitiesSubscription()
@@ -347,6 +237,7 @@ actor SpotifyAppRemoteCoordinator {
347
237
 
348
238
  func didFailToConnect(error: Error?) {
349
239
  appRemote = nil
240
+ pendingAuthorizeRemote = nil
350
241
  let msg = error.map { describeNSError($0 as NSError) } ?? "Unknown connection failure"
351
242
  let remoteError = AppRemoteError.connectionFailed(msg)
352
243
  transitionState("disconnected")
@@ -381,7 +272,7 @@ actor SpotifyAppRemoteCoordinator {
381
272
 
382
273
  /// Called by `SpotifyPlayerStateDelegateBridge` when a state update arrives.
383
274
  func playerStateDidChange(_ state: any SPTAppRemotePlayerState) {
384
- onPlayerStateChange?(Self.playerStateToMap(state))
275
+ onPlayerStateChange?(SpotifyAppRemoteMappers.playerStateToMap(state))
385
276
  }
386
277
 
387
278
  // MARK: — User subscription
@@ -400,7 +291,7 @@ actor SpotifyAppRemoteCoordinator {
400
291
 
401
292
  /// Called by `SpotifyUserCapabilitiesDelegateBridge` on each capabilities update.
402
293
  func userCapabilitiesDidChange(_ capabilities: any SPTAppRemoteUserCapabilities) {
403
- onCapabilitiesChange?(Self.capabilitiesToMap(capabilities))
294
+ onCapabilitiesChange?(SpotifyAppRemoteMappers.capabilitiesToMap(capabilities))
404
295
  }
405
296
 
406
297
  // MARK: — Player transport
@@ -458,9 +349,9 @@ actor SpotifyAppRemoteCoordinator {
458
349
  return try await withCheckedThrowingContinuation { cont in
459
350
  playerAPI.getPlayerState { result, error in
460
351
  if let error = error {
461
- cont.resume(throwing: Self.normalizePlayerError(error as NSError, callsite: "Player.getPlayerState"))
352
+ cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapPlayerError(error as NSError, callsite: "Player.getPlayerState"))
462
353
  } else if let state = result as? any SPTAppRemotePlayerState {
463
- cont.resume(returning: Self.playerStateToMap(state))
354
+ cont.resume(returning: SpotifyAppRemoteMappers.playerStateToMap(state))
464
355
  } else {
465
356
  cont.resume(throwing: NativePlayerError.unknown("Player.getPlayerState: unexpected result type"))
466
357
  }
@@ -473,7 +364,7 @@ actor SpotifyAppRemoteCoordinator {
473
364
  return try await withCheckedThrowingContinuation { cont in
474
365
  playerAPI.getCrossfadeState { result, error in
475
366
  if let error = error {
476
- cont.resume(throwing: Self.normalizePlayerError(error as NSError, callsite: "Player.getCrossfadeState"))
367
+ cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapPlayerError(error as NSError, callsite: "Player.getCrossfadeState"))
477
368
  } else if let state = result as? any SPTAppRemoteCrossfadeState {
478
369
  cont.resume(returning: ["isEnabled": state.isEnabled, "duration": state.duration])
479
370
  } else {
@@ -489,7 +380,7 @@ actor SpotifyAppRemoteCoordinator {
489
380
  let speeds: [any SPTAppRemotePodcastPlaybackSpeed] = try await withCheckedThrowingContinuation { cont in
490
381
  playerAPI.getAvailablePodcastPlaybackSpeeds { result, error in
491
382
  if let error = error {
492
- cont.resume(throwing: Self.normalizePlayerError(error as NSError, callsite: "Player.setPodcastPlaybackSpeed"))
383
+ cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapPlayerError(error as NSError, callsite: "Player.setPodcastPlaybackSpeed"))
493
384
  } else if let speeds = result as? [any SPTAppRemotePodcastPlaybackSpeed] {
494
385
  cont.resume(returning: speeds)
495
386
  } else {
@@ -515,9 +406,9 @@ actor SpotifyAppRemoteCoordinator {
515
406
  return try await withCheckedThrowingContinuation { cont in
516
407
  userAPI.fetchCapabilities { result, error in
517
408
  if let error = error {
518
- cont.resume(throwing: Self.normalizeUserError(error as NSError, callsite: "User.getCapabilities"))
409
+ cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapUserError(error as NSError, callsite: "User.getCapabilities"))
519
410
  } else if let capabilities = result as? any SPTAppRemoteUserCapabilities {
520
- cont.resume(returning: Self.capabilitiesToMap(capabilities))
411
+ cont.resume(returning: SpotifyAppRemoteMappers.capabilitiesToMap(capabilities))
521
412
  } else {
522
413
  cont.resume(throwing: NativeUserError.unknown("User.getCapabilities: unexpected result type"))
523
414
  }
@@ -530,9 +421,9 @@ actor SpotifyAppRemoteCoordinator {
530
421
  return try await withCheckedThrowingContinuation { cont in
531
422
  userAPI.fetchLibraryState(forURI: uri) { result, error in
532
423
  if let error = error {
533
- cont.resume(throwing: Self.normalizeUserError(error as NSError, callsite: "User.getLibraryState"))
424
+ cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapUserError(error as NSError, callsite: "User.getLibraryState"))
534
425
  } else if let state = result as? any SPTAppRemoteLibraryState {
535
- cont.resume(returning: Self.libraryStateToMap(state))
426
+ cont.resume(returning: SpotifyAppRemoteMappers.libraryStateToMap(state))
536
427
  } else {
537
428
  cont.resume(throwing: NativeUserError.unknown("User.getLibraryState: unexpected result type"))
538
429
  }
@@ -545,9 +436,9 @@ actor SpotifyAppRemoteCoordinator {
545
436
  return try await withCheckedThrowingContinuation { cont in
546
437
  userAPI.addItemToLibrary(withURI: uri) { result, error in
547
438
  if let error = error {
548
- cont.resume(throwing: Self.normalizeUserError(error as NSError, callsite: "User.addToLibrary"))
439
+ cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapUserError(error as NSError, callsite: "User.addToLibrary"))
549
440
  } else if let state = result as? any SPTAppRemoteLibraryState {
550
- cont.resume(returning: Self.libraryStateToMap(state))
441
+ cont.resume(returning: SpotifyAppRemoteMappers.libraryStateToMap(state))
551
442
  } else {
552
443
  cont.resume(throwing: NativeUserError.unknown("User.addToLibrary: unexpected result type"))
553
444
  }
@@ -560,9 +451,9 @@ actor SpotifyAppRemoteCoordinator {
560
451
  return try await withCheckedThrowingContinuation { cont in
561
452
  userAPI.removeItemFromLibrary(withURI: uri) { result, error in
562
453
  if let error = error {
563
- cont.resume(throwing: Self.normalizeUserError(error as NSError, callsite: "User.removeFromLibrary"))
454
+ cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapUserError(error as NSError, callsite: "User.removeFromLibrary"))
564
455
  } else if let state = result as? any SPTAppRemoteLibraryState {
565
- cont.resume(returning: Self.libraryStateToMap(state))
456
+ cont.resume(returning: SpotifyAppRemoteMappers.libraryStateToMap(state))
566
457
  } else {
567
458
  cont.resume(throwing: NativeUserError.unknown("User.removeFromLibrary: unexpected result type"))
568
459
  }
@@ -575,11 +466,11 @@ actor SpotifyAppRemoteCoordinator {
575
466
  func contentGetRecommendedContentItems(type: String) async throws -> [[String: Any]] {
576
467
  let contentAPI = try requireContentAPI(callsite: "Content.getRecommendedContentItems")
577
468
  return try await withCheckedThrowingContinuation { cont in
578
- contentAPI.fetchRecommendedContentItems(forType: Self.mapContentType(type), flattenContainers: false) { result, error in
469
+ contentAPI.fetchRecommendedContentItems(forType: SpotifyAppRemoteMappers.mapContentType(type), flattenContainers: false) { result, error in
579
470
  if let error = error {
580
- cont.resume(throwing: Self.normalizeContentError(error as NSError, callsite: "Content.getRecommendedContentItems"))
471
+ cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapContentError(error as NSError, callsite: "Content.getRecommendedContentItems"))
581
472
  } else if let list = result as? [any SPTAppRemoteContentItem] {
582
- cont.resume(returning: list.map(Self.contentItemToMap))
473
+ cont.resume(returning: list.map(SpotifyAppRemoteMappers.contentItemToMap))
583
474
  } else {
584
475
  cont.resume(throwing: NativeContentError.unknown("Content.getRecommendedContentItems: unexpected result type"))
585
476
  }
@@ -596,7 +487,7 @@ actor SpotifyAppRemoteCoordinator {
596
487
  let contentItem: any SPTAppRemoteContentItem = try await withCheckedThrowingContinuation { cont in
597
488
  contentAPI.fetchContentItem(forURI: uri) { result, error in
598
489
  if let error = error {
599
- cont.resume(throwing: Self.normalizeContentError(error as NSError, callsite: "Content.getChildren"))
490
+ cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapContentError(error as NSError, callsite: "Content.getChildren"))
600
491
  } else if let item = result as? any SPTAppRemoteContentItem {
601
492
  cont.resume(returning: item)
602
493
  } else {
@@ -608,9 +499,9 @@ actor SpotifyAppRemoteCoordinator {
608
499
  return try await withCheckedThrowingContinuation { cont in
609
500
  contentAPI.fetchChildren(of: contentItem, callback: { result, error in
610
501
  if let error = error {
611
- cont.resume(throwing: Self.normalizeContentError(error as NSError, callsite: "Content.getChildren"))
502
+ cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapContentError(error as NSError, callsite: "Content.getChildren"))
612
503
  } else if let children = result as? [any SPTAppRemoteContentItem] {
613
- cont.resume(returning: children.map(Self.contentItemToMap))
504
+ cont.resume(returning: children.map(SpotifyAppRemoteMappers.contentItemToMap))
614
505
  } else {
615
506
  cont.resume(throwing: NativeContentError.unknown("Content.getChildren: unexpected result type"))
616
507
  }
@@ -626,12 +517,12 @@ actor SpotifyAppRemoteCoordinator {
626
517
  }
627
518
  let imageAPI = try requireImageAPI(callsite: "Images.load")
628
519
  let representable = LocalImageRepresentable(imageIdentifier: imageIdentifier)
629
- let targetSize = Self.mapImageSize(size)
520
+ let targetSize = SpotifyAppRemoteMappers.mapImageSize(size)
630
521
 
631
522
  let image: UIImage = try await withCheckedThrowingContinuation { cont in
632
523
  imageAPI.fetchImage(forItem: representable, with: targetSize) { result, error in
633
524
  if let error = error {
634
- cont.resume(throwing: Self.normalizeImagesError(error as NSError, callsite: "Images.load"))
525
+ cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapImagesError(error as NSError, callsite: "Images.load"))
635
526
  } else if let image = result as? UIImage {
636
527
  cont.resume(returning: image)
637
528
  } else {
@@ -699,7 +590,7 @@ actor SpotifyAppRemoteCoordinator {
699
590
  try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
700
591
  block { _, error in
701
592
  if let error = error {
702
- cont.resume(throwing: Self.normalizePlayerError(error as NSError, callsite: callsite))
593
+ cont.resume(throwing: SpotifyAppRemoteErrorMapping.mapPlayerError(error as NSError, callsite: callsite))
703
594
  } else {
704
595
  cont.resume()
705
596
  }
@@ -707,164 +598,6 @@ actor SpotifyAppRemoteCoordinator {
707
598
  }
708
599
  }
709
600
 
710
- private static func normalizePlayerError(_ error: NSError, callsite: String) -> NativePlayerError {
711
- guard error.domain == SPTAppRemoteErrorDomain else {
712
- return .unknown(error.localizedDescription)
713
- }
714
- switch error.code {
715
- case SPTAppRemoteErrorCode.connectionTerminatedError.rawValue:
716
- return .connectionLost("\(callsite): connection to Spotify app was terminated")
717
- case SPTAppRemoteErrorCode.invalidArgumentsError.rawValue:
718
- return .invalidParameter(error.localizedDescription)
719
- case SPTAppRemoteErrorCode.requestFailedError.rawValue:
720
- let desc = error.localizedDescription.lowercased()
721
- if desc.contains("premium") {
722
- return .premiumRequired("\(callsite): Spotify Premium is required for on-demand playback")
723
- }
724
- if desc.contains("not allowed") || desc.contains("restriction") {
725
- return .operationNotAllowed("\(callsite): \(error.localizedDescription)")
726
- }
727
- return .unknown(error.localizedDescription)
728
- default:
729
- return .unknown(error.localizedDescription)
730
- }
731
- }
732
-
733
- private static func playerStateToMap(_ state: any SPTAppRemotePlayerState) -> [String: Any] {
734
- let track = state.track
735
- let restrictions = state.playbackRestrictions
736
- let options = state.playbackOptions
737
- return [
738
- "track": [
739
- "uri": track.uri,
740
- "name": track.name,
741
- "imageIdentifier": track.imageIdentifier,
742
- "duration": track.duration,
743
- "artist": ["name": track.artist.name, "uri": track.artist.uri],
744
- "album": ["name": track.album.name, "uri": track.album.uri],
745
- "isSaved": track.isSaved,
746
- "isEpisode": track.isEpisode,
747
- "isPodcast": track.isPodcast,
748
- "isAdvertisement": track.isAdvertisement,
749
- ] as [String: Any],
750
- "playbackPosition": state.playbackPosition,
751
- "playbackSpeed": state.playbackSpeed,
752
- "isPaused": state.isPaused,
753
- "playbackOptions": [
754
- "isShuffling": options.isShuffling,
755
- "repeatMode": options.repeatMode.rawValue,
756
- ] as [String: Any],
757
- "playbackRestrictions": [
758
- "canSkipNext": restrictions.canSkipNext,
759
- "canSkipPrevious": restrictions.canSkipPrevious,
760
- "canRepeatTrack": restrictions.canRepeatTrack,
761
- "canRepeatContext": restrictions.canRepeatContext,
762
- "canToggleShuffle": restrictions.canToggleShuffle,
763
- "canSeek": restrictions.canSeek,
764
- ] as [String: Any],
765
- "contextTitle": state.contextTitle,
766
- "contextUri": state.contextURI.absoluteString,
767
- ]
768
- }
769
-
770
- private static func capabilitiesToMap(_ capabilities: any SPTAppRemoteUserCapabilities) -> [String: Any] {
771
- ["canPlayOnDemand": capabilities.canPlayOnDemand]
772
- }
773
-
774
- private static func libraryStateToMap(_ state: any SPTAppRemoteLibraryState) -> [String: Any] {
775
- ["uri": state.uri, "isAdded": state.isAdded, "canAdd": state.canAdd]
776
- }
777
-
778
- private static func normalizeUserError(_ error: NSError, callsite: String) -> NativeUserError {
779
- guard error.domain == SPTAppRemoteErrorDomain else {
780
- return .unknown(error.localizedDescription)
781
- }
782
- switch error.code {
783
- case SPTAppRemoteErrorCode.connectionTerminatedError.rawValue:
784
- return .connectionLost("\(callsite): connection to Spotify app was terminated")
785
- case SPTAppRemoteErrorCode.invalidArgumentsError.rawValue:
786
- return .invalidURI("\(callsite): \(error.localizedDescription)")
787
- case SPTAppRemoteErrorCode.requestFailedError.rawValue:
788
- let desc = error.localizedDescription.lowercased()
789
- if desc.contains("not allowed") || desc.contains("restriction") {
790
- return .operationNotAllowed("\(callsite): \(error.localizedDescription)")
791
- }
792
- return .unknown(error.localizedDescription)
793
- default:
794
- return .unknown(error.localizedDescription)
795
- }
796
- }
797
-
798
- private static func normalizeContentError(_ error: NSError, callsite: String) -> NativeContentError {
799
- guard error.domain == SPTAppRemoteErrorDomain else {
800
- return .unknown(error.localizedDescription)
801
- }
802
- switch error.code {
803
- case SPTAppRemoteErrorCode.connectionTerminatedError.rawValue:
804
- return .connectionLost("\(callsite): connection to Spotify app was terminated")
805
- case SPTAppRemoteErrorCode.requestFailedError.rawValue:
806
- let desc = error.localizedDescription.lowercased()
807
- if desc.contains("not supported") || desc.contains("unsupported") {
808
- return .contentAPIUnavailable("\(callsite): content API is unavailable on this Spotify app version")
809
- }
810
- return .unknown(error.localizedDescription)
811
- default:
812
- return .unknown(error.localizedDescription)
813
- }
814
- }
815
-
816
- private static func normalizeImagesError(_ error: NSError, callsite: String) -> NativeImagesError {
817
- guard error.domain == SPTAppRemoteErrorDomain else {
818
- return .unknown(error.localizedDescription)
819
- }
820
- switch error.code {
821
- case SPTAppRemoteErrorCode.connectionTerminatedError.rawValue:
822
- return .notConnected("\(callsite): connection to Spotify app was terminated")
823
- case SPTAppRemoteErrorCode.invalidArgumentsError.rawValue:
824
- return .invalidURI("\(callsite): invalid image identifier")
825
- case SPTAppRemoteErrorCode.requestFailedError.rawValue:
826
- return .imageLoadFailed("\(callsite): Spotify rejected image request")
827
- default:
828
- return .unknown(error.localizedDescription)
829
- }
830
- }
831
-
832
- private static func mapContentType(_ type: String) -> String {
833
- switch type {
834
- case "navigation": return SPTAppRemoteContentTypeNavigation
835
- case "fitness": return SPTAppRemoteContentTypeFitness
836
- case "gaming": return SPTAppRemoteContentTypeGaming
837
- default: return SPTAppRemoteContentTypeDefault
838
- }
839
- }
840
-
841
- private static func mapImageSize(_ size: String) -> CGSize {
842
- switch size {
843
- case "small": return CGSize(width: 64, height: 64)
844
- case "medium": return CGSize(width: 300, height: 300)
845
- default: return CGSize(width: 640, height: 640)
846
- }
847
- }
848
-
849
- private static func contentItemToMap(_ item: any SPTAppRemoteContentItem) -> [String: Any] {
850
- var map: [String: Any] = [
851
- "title": item.title as Any,
852
- "subtitle": item.subtitle as Any,
853
- "contentDescription": item.contentDescription as Any,
854
- "identifier": item.identifier,
855
- "uri": item.uri,
856
- "imageIdentifier": item.imageIdentifier,
857
- "isAvailableOffline": item.isAvailableOffline,
858
- "isPlayable": item.isPlayable,
859
- "isContainer": item.isContainer,
860
- "isPinned": item.isPinned,
861
- ]
862
- if let children = item.children {
863
- map["children"] = children.map(Self.contentItemToMap)
864
- }
865
- return map
866
- }
867
-
868
601
  private func transitionState(_ state: String) {
869
602
  connectionStateString = state
870
603
  onConnectionStateChange?(state)