@wwdrew/expo-spotify-sdk 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -3
- package/README.md +346 -96
- package/android/build.gradle +28 -22
- package/android/consumer-rules.pro +8 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +133 -147
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyAuthCoordinator.kt +36 -0
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyAuthorizationContract.kt +46 -0
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyConfig.kt +84 -0
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyErrors.kt +34 -0
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyTokenSwapClient.kt +154 -0
- package/build/ExpoSpotifySDK.types.d.ts +84 -1
- package/build/ExpoSpotifySDK.types.d.ts.map +1 -1
- package/build/ExpoSpotifySDK.types.js +12 -1
- package/build/ExpoSpotifySDK.types.js.map +1 -1
- package/build/ExpoSpotifySDKModule.web.d.ts +7 -3
- package/build/ExpoSpotifySDKModule.web.d.ts.map +1 -1
- package/build/ExpoSpotifySDKModule.web.js +17 -7
- package/build/ExpoSpotifySDKModule.web.js.map +1 -1
- package/build/index.d.ts +39 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +117 -4
- package/build/index.js.map +1 -1
- package/expo-module.config.json +1 -1
- package/ios/ExpoSpotifyAppDelegate.swift +14 -0
- package/ios/ExpoSpotifyConfiguration.swift +33 -15
- package/ios/ExpoSpotifySDK.podspec +2 -4
- package/ios/ExpoSpotifySDKModule.swift +98 -26
- package/ios/SPTScopeSerializer.swift +40 -39
- package/ios/SpotifyAuthCoordinator.swift +186 -0
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTConfiguration.h +1 -1
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTError.h +6 -7
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTScope.h +43 -39
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTSessionManager.h +8 -4
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SpotifyiOS.h +1 -1
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Info.plist +0 -0
- 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/SPTConfiguration.h +1 -1
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTError.h +6 -7
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTScope.h +43 -39
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTSessionManager.h +8 -4
- package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SpotifyiOS.h +1 -1
- 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/SpotifyiOS +0 -0
- package/ios/SpotifyTokenRefreshClient.swift +118 -0
- package/package.json +12 -11
- package/plugin/build/android/withSpotifyAndroidAppBuildGradle.js +15 -10
- package/plugin/build/ios/withSpotifyConfigValues.js +12 -7
- package/plugin/build/ios/withSpotifyURLScheme.d.ts +1 -1
- package/plugin/build/ios/withSpotifyURLScheme.js +9 -8
- package/plugin/build/types.d.ts +35 -0
- package/plugin/build/types.js +0 -19
- package/ios/ExpoSpotifyAuthDelegate.swift +0 -13
- package/ios/ExpoSpotifySessionManager+SessionManagerDelegate.swift +0 -16
- package/ios/ExpoSpotifySessionManager.swift +0 -90
- package/ios/SpotifySDK/SpotifyiOS.xcframework/_CodeSignature/CodeDirectory +0 -0
- package/ios/SpotifySDK/SpotifyiOS.xcframework/_CodeSignature/CodeRequirements +0 -0
- package/ios/SpotifySDK/SpotifyiOS.xcframework/_CodeSignature/CodeRequirements-1 +0 -0
- package/ios/SpotifySDK/SpotifyiOS.xcframework/_CodeSignature/CodeResources +0 -1118
- package/ios/SpotifySDK/SpotifyiOS.xcframework/_CodeSignature/CodeSignature +0 -0
|
@@ -1,37 +1,109 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
2
|
import SpotifyiOS
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
public func definition() -> ModuleDefinition {
|
|
4
|
+
private let SDK_VERSION = "0.5.0"
|
|
5
|
+
private let EVENT_SESSION_CHANGE = "onSessionChange"
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
public class ExpoSpotifySDKModule: Module {
|
|
8
|
+
public func definition() -> ModuleDefinition {
|
|
9
|
+
Name("ExpoSpotifySDK")
|
|
10
|
+
Events(EVENT_SESSION_CHANGE)
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
Function("isAvailable") { () -> Bool in
|
|
13
|
+
SpotifyAuthCoordinator.shared?.isSpotifyAppInstalled() ?? false
|
|
14
|
+
}
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
AsyncFunction("authenticateAsync") { (config: AuthenticateOptions) async throws -> [String: Any?] in
|
|
17
|
+
do {
|
|
18
|
+
guard let coordinator = SpotifyAuthCoordinator.shared else {
|
|
19
|
+
throw SpotifyError.invalidConfiguration(
|
|
20
|
+
"Missing `ExpoSpotifySDK` configuration in Info.plist. " +
|
|
21
|
+
"Did you add @wwdrew/expo-spotify-sdk to your Expo plugins?"
|
|
22
|
+
)
|
|
14
23
|
}
|
|
24
|
+
if config.scopes.isEmpty {
|
|
25
|
+
throw SpotifyError.invalidConfiguration("`scopes` must contain at least one entry")
|
|
26
|
+
}
|
|
27
|
+
let scopes = SPTScopeSerializer.deserialize(config.scopes)
|
|
28
|
+
let session = try await coordinator.authenticate(
|
|
29
|
+
scopes: scopes,
|
|
30
|
+
tokenSwapURL: config.tokenSwapURL.flatMap(URL.init),
|
|
31
|
+
tokenRefreshURL: config.tokenRefreshURL.flatMap(URL.init),
|
|
32
|
+
showDialog: config.showDialog
|
|
33
|
+
)
|
|
34
|
+
let map = self.sessionToMap(session)
|
|
35
|
+
self.sendEvent(EVENT_SESSION_CHANGE, ["type": "didInitiate", "session": map])
|
|
36
|
+
return map
|
|
37
|
+
} catch let error as SpotifyError {
|
|
38
|
+
self.sendEvent(EVENT_SESSION_CHANGE, [
|
|
39
|
+
"type": "didFail",
|
|
40
|
+
"error": ["code": error.code, "message": error.message],
|
|
41
|
+
])
|
|
42
|
+
throw GenericException("\(error.code): \(error.message)")
|
|
43
|
+
}
|
|
44
|
+
}
|
|
15
45
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
spotifySession.authenticate(scopes: scopes, tokenSwapURL: tokenSwapURL, tokenRefreshURL: tokenRefreshURL).done { session in
|
|
26
|
-
promise.resolve([
|
|
27
|
-
"accessToken": session.accessToken,
|
|
28
|
-
"refreshToken": session.refreshToken,
|
|
29
|
-
"expirationDate": Int(session.expirationDate.timeIntervalSince1970 * 1000),
|
|
30
|
-
"scopes": SPTScopeSerializer.serializeScopes(session.scope)
|
|
31
|
-
])
|
|
32
|
-
}.catch { error in
|
|
33
|
-
promise.reject(error)
|
|
34
|
-
}
|
|
46
|
+
AsyncFunction("refreshSessionAsync") { (options: RefreshOptions) async throws -> [String: Any?] in
|
|
47
|
+
do {
|
|
48
|
+
guard let configuration = ExpoSpotifyConfiguration.fromInfoPlist() else {
|
|
49
|
+
throw SpotifyError.invalidConfiguration(
|
|
50
|
+
"Missing `ExpoSpotifySDK` configuration in Info.plist."
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
if options.refreshToken.isEmpty {
|
|
54
|
+
throw SpotifyError.invalidConfiguration("`refreshToken` is required")
|
|
35
55
|
}
|
|
56
|
+
if options.tokenRefreshURL.isEmpty {
|
|
57
|
+
throw SpotifyError.invalidConfiguration("`tokenRefreshURL` is required")
|
|
58
|
+
}
|
|
59
|
+
let client = SpotifyTokenRefreshClient(sdkVersion: SDK_VERSION, clientID: configuration.clientID)
|
|
60
|
+
let result = try await client.refresh(
|
|
61
|
+
refreshToken: options.refreshToken,
|
|
62
|
+
tokenRefreshURL: options.tokenRefreshURL,
|
|
63
|
+
previousScopes: []
|
|
64
|
+
)
|
|
65
|
+
let map: [String: Any?] = [
|
|
66
|
+
"accessToken": result.accessToken,
|
|
67
|
+
"refreshToken": result.refreshToken,
|
|
68
|
+
"expirationDate": result.expirationDate,
|
|
69
|
+
"scopes": result.scopes,
|
|
70
|
+
]
|
|
71
|
+
self.sendEvent(EVENT_SESSION_CHANGE, ["type": "didRenew", "session": map])
|
|
72
|
+
return map
|
|
73
|
+
} catch let error as SpotifyError {
|
|
74
|
+
self.sendEvent(EVENT_SESSION_CHANGE, [
|
|
75
|
+
"type": "didFail",
|
|
76
|
+
"error": ["code": error.code, "message": error.message],
|
|
77
|
+
])
|
|
78
|
+
throw GenericException("\(error.code): \(error.message)")
|
|
79
|
+
} catch let error as SpotifyRefreshError {
|
|
80
|
+
self.sendEvent(EVENT_SESSION_CHANGE, [
|
|
81
|
+
"type": "didFail",
|
|
82
|
+
"error": ["code": error.code, "message": error.message],
|
|
83
|
+
])
|
|
84
|
+
throw GenericException("\(error.code): \(error.message)")
|
|
85
|
+
}
|
|
36
86
|
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private func sessionToMap(_ session: SPTSession) -> [String: Any?] {
|
|
90
|
+
return [
|
|
91
|
+
"accessToken": session.accessToken,
|
|
92
|
+
"refreshToken": session.refreshToken.isEmpty ? nil : session.refreshToken,
|
|
93
|
+
"expirationDate": Int(session.expirationDate.timeIntervalSince1970 * 1000),
|
|
94
|
+
"scopes": SPTScopeSerializer.serialize(session.scope),
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
struct AuthenticateOptions: Record {
|
|
100
|
+
@Field var scopes: [String] = []
|
|
101
|
+
@Field var tokenSwapURL: String? = nil
|
|
102
|
+
@Field var tokenRefreshURL: String? = nil
|
|
103
|
+
@Field var showDialog: Bool = false
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
struct RefreshOptions: Record {
|
|
107
|
+
@Field var refreshToken: String = ""
|
|
108
|
+
@Field var tokenRefreshURL: String = ""
|
|
37
109
|
}
|
|
@@ -1,44 +1,45 @@
|
|
|
1
|
+
import Foundation
|
|
1
2
|
import SpotifyiOS
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
return serializedScopes
|
|
4
|
+
enum SPTScopeSerializer {
|
|
5
|
+
/// Single source of truth for the string ↔ SPTScope mapping. Order is
|
|
6
|
+
/// preserved on round-trip via `serialize` (sorted alphabetically) so output
|
|
7
|
+
/// is deterministic.
|
|
8
|
+
static let scopeMap: [(String, SPTScope)] = [
|
|
9
|
+
("app-remote-control", .appRemoteControl),
|
|
10
|
+
("playlist-modify-private", .playlistModifyPrivate),
|
|
11
|
+
("playlist-modify-public", .playlistModifyPublic),
|
|
12
|
+
("playlist-read-collaborative", .playlistReadCollaborative),
|
|
13
|
+
("playlist-read-private", .playlistReadPrivate),
|
|
14
|
+
("streaming", .streaming),
|
|
15
|
+
("ugc-image-upload", .ugcImageUpload),
|
|
16
|
+
("user-follow-modify", .userFollowModify),
|
|
17
|
+
("user-follow-read", .userFollowRead),
|
|
18
|
+
("user-library-modify", .userLibraryModify),
|
|
19
|
+
("user-library-read", .userLibraryRead),
|
|
20
|
+
("user-modify-playback-state", .userModifyPlaybackState),
|
|
21
|
+
("user-read-currently-playing", .userReadCurrentlyPlaying),
|
|
22
|
+
("user-read-email", .userReadEmail),
|
|
23
|
+
("user-read-playback-state", .userReadPlaybackState),
|
|
24
|
+
("user-read-private", .userReadPrivate),
|
|
25
|
+
("user-read-recently-played", .userReadRecentlyPlayed),
|
|
26
|
+
("user-top-read", .userTopRead),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
static func serialize(_ scopes: SPTScope) -> [String] {
|
|
30
|
+
scopeMap.compactMap { (string, value) in
|
|
31
|
+
scopes.contains(value) ? string : nil
|
|
33
32
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static func deserialize(_ scopes: [String]) -> SPTScope {
|
|
36
|
+
var result: SPTScope = []
|
|
37
|
+
let lookup = Dictionary(uniqueKeysWithValues: scopeMap)
|
|
38
|
+
for string in scopes {
|
|
39
|
+
if let value = lookup[string] {
|
|
40
|
+
result.insert(value)
|
|
41
|
+
}
|
|
43
42
|
}
|
|
43
|
+
return result
|
|
44
|
+
}
|
|
44
45
|
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SpotifyiOS
|
|
3
|
+
|
|
4
|
+
/// Public, structured errors thrown by the coordinator. Each case carries the
|
|
5
|
+
/// JS-facing error code in `code`, mirroring the Android `CodedException`
|
|
6
|
+
/// taxonomy.
|
|
7
|
+
enum SpotifyError: Error {
|
|
8
|
+
case invalidConfiguration(String)
|
|
9
|
+
case authInProgress
|
|
10
|
+
case userCancelled
|
|
11
|
+
case spotifyNotInstalled
|
|
12
|
+
case underlying(Error)
|
|
13
|
+
|
|
14
|
+
var code: String {
|
|
15
|
+
switch self {
|
|
16
|
+
case .invalidConfiguration: return "INVALID_CONFIG"
|
|
17
|
+
case .authInProgress: return "AUTH_IN_PROGRESS"
|
|
18
|
+
case .userCancelled: return "USER_CANCELLED"
|
|
19
|
+
case .spotifyNotInstalled: return "SPOTIFY_NOT_INSTALLED"
|
|
20
|
+
case .underlying: return "UNKNOWN"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var message: String {
|
|
25
|
+
switch self {
|
|
26
|
+
case .invalidConfiguration(let m): return m
|
|
27
|
+
case .authInProgress: return "Another authentication request is already in progress"
|
|
28
|
+
case .userCancelled: return "Authentication was cancelled by the user"
|
|
29
|
+
case .spotifyNotInstalled: return "The Spotify app is not installed on this device"
|
|
30
|
+
case .underlying(let err): return err.localizedDescription
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// `actor` ensures `pending` is mutated only on the actor's serial executor;
|
|
36
|
+
/// no manual locks needed.
|
|
37
|
+
actor SpotifyAuthCoordinator {
|
|
38
|
+
static let shared: SpotifyAuthCoordinator? = SpotifyAuthCoordinator.create()
|
|
39
|
+
|
|
40
|
+
private let sptConfiguration: SPTConfiguration
|
|
41
|
+
private let sessionManager: SPTSessionManager
|
|
42
|
+
private let bridge: SpotifySessionDelegateBridge
|
|
43
|
+
private var pending: CheckedContinuation<SPTSession, Error>?
|
|
44
|
+
|
|
45
|
+
private init(sptConfiguration: SPTConfiguration, sessionManager: SPTSessionManager, bridge: SpotifySessionDelegateBridge) {
|
|
46
|
+
self.sptConfiguration = sptConfiguration
|
|
47
|
+
self.sessionManager = sessionManager
|
|
48
|
+
self.bridge = bridge
|
|
49
|
+
bridge.coordinator = self
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private static func create() -> SpotifyAuthCoordinator? {
|
|
53
|
+
guard
|
|
54
|
+
let configuration = ExpoSpotifyConfiguration.fromInfoPlist(),
|
|
55
|
+
let sptConfig = configuration.sptConfiguration
|
|
56
|
+
else {
|
|
57
|
+
NSLog("[ExpoSpotifySDK] Missing or invalid `ExpoSpotifySDK` Info.plist entry")
|
|
58
|
+
return nil
|
|
59
|
+
}
|
|
60
|
+
let bridge = SpotifySessionDelegateBridge()
|
|
61
|
+
let manager = SPTSessionManager(configuration: sptConfig, delegate: bridge)
|
|
62
|
+
return SpotifyAuthCoordinator(sptConfiguration: sptConfig, sessionManager: manager, bridge: bridge)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Synchronous getter used by the module's `Function("isAvailable")`. Safe
|
|
66
|
+
/// to call from any thread because the underlying SDK property is read-only
|
|
67
|
+
/// state derived from the installed-apps query.
|
|
68
|
+
nonisolated func isSpotifyAppInstalled() -> Bool {
|
|
69
|
+
sessionManager.isSpotifyAppInstalled
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Synchronous URL-handler dispatcher used by the AppDelegate / SceneDelegate
|
|
73
|
+
/// subscribers to forward redirects to the SPTSessionManager.
|
|
74
|
+
nonisolated func handleOpenURL(_ url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
|
|
75
|
+
sessionManager.application(UIApplication.shared, open: url, options: options)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func authenticate(
|
|
79
|
+
scopes: SPTScope,
|
|
80
|
+
tokenSwapURL: URL?,
|
|
81
|
+
tokenRefreshURL: URL?,
|
|
82
|
+
showDialog: Bool = false
|
|
83
|
+
) async throws -> SPTSession {
|
|
84
|
+
guard pending == nil else { throw SpotifyError.authInProgress }
|
|
85
|
+
|
|
86
|
+
// SPTConfiguration is a reference type shared with the session manager, so
|
|
87
|
+
// mutating it here takes effect for the upcoming initiateSession call.
|
|
88
|
+
sptConfiguration.tokenSwapURL = tokenSwapURL
|
|
89
|
+
sptConfiguration.tokenRefreshURL = tokenRefreshURL
|
|
90
|
+
// alwaysShowAuthorizationDialog is a property on SPTSessionManager, not an
|
|
91
|
+
// SPTAuthorizationOptions flag (the options enum has no such case).
|
|
92
|
+
sessionManager.alwaysShowAuthorizationDialog = showDialog
|
|
93
|
+
|
|
94
|
+
NSLog(
|
|
95
|
+
"[ExpoSpotifySDK] initiateSession redirectURL=%@ tokenSwapURL=%@ tokenRefreshURL=%@ showDialog=%d",
|
|
96
|
+
sptConfiguration.redirectURL.absoluteString,
|
|
97
|
+
tokenSwapURL?.absoluteString ?? "nil",
|
|
98
|
+
tokenRefreshURL?.absoluteString ?? "nil",
|
|
99
|
+
showDialog ? 1 : 0
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return try await withCheckedThrowingContinuation { (cont: CheckedContinuation<SPTSession, Error>) in
|
|
103
|
+
self.pending = cont
|
|
104
|
+
Task { @MainActor in
|
|
105
|
+
self.sessionManager.initiateSession(with: scopes, options: .default, campaign: nil)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func deliver(_ result: Result<SPTSession, Error>) {
|
|
111
|
+
let cont = pending
|
|
112
|
+
pending = nil
|
|
113
|
+
cont?.resume(with: result)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// `SPTSessionManagerDelegate` requires NSObject conformance, which an actor
|
|
118
|
+
/// can't satisfy directly. The bridge is a tiny NSObject that hops the call
|
|
119
|
+
/// onto the actor's executor.
|
|
120
|
+
final class SpotifySessionDelegateBridge: NSObject, SPTSessionManagerDelegate {
|
|
121
|
+
weak var coordinator: SpotifyAuthCoordinator?
|
|
122
|
+
|
|
123
|
+
func sessionManager(manager _: SPTSessionManager, didInitiate session: SPTSession) {
|
|
124
|
+
NSLog(
|
|
125
|
+
"[ExpoSpotifySDK] didInitiate accessToken.length=%d refreshToken.length=%d expirationDate=%@ scope=%lu",
|
|
126
|
+
session.accessToken.count,
|
|
127
|
+
session.refreshToken.count,
|
|
128
|
+
session.expirationDate as NSDate,
|
|
129
|
+
session.scope.rawValue
|
|
130
|
+
)
|
|
131
|
+
Task { await coordinator?.deliver(.success(session)) }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func sessionManager(manager _: SPTSessionManager, didFailWith error: Error) {
|
|
135
|
+
let mapped: Error = mapSDKError(error)
|
|
136
|
+
NSLog("[ExpoSpotifySDK] didFailWithError %@", String(describing: error))
|
|
137
|
+
Task { await coordinator?.deliver(.failure(mapped)) }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func sessionManager(manager _: SPTSessionManager, didRenew session: SPTSession) {
|
|
141
|
+
NSLog(
|
|
142
|
+
"[ExpoSpotifySDK] didRenew accessToken.length=%d refreshToken.length=%d",
|
|
143
|
+
session.accessToken.count,
|
|
144
|
+
session.refreshToken.count
|
|
145
|
+
)
|
|
146
|
+
Task { await coordinator?.deliver(.success(session)) }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// Translate a few well-known SDK error shapes into our taxonomy.
|
|
150
|
+
private func mapSDKError(_ error: Error) -> Error {
|
|
151
|
+
let nsError = error as NSError
|
|
152
|
+
let description = nsError.localizedDescription.lowercased()
|
|
153
|
+
if description.contains("cancel") {
|
|
154
|
+
return SpotifyError.userCancelled
|
|
155
|
+
}
|
|
156
|
+
return SpotifyError.underlying(NSError(
|
|
157
|
+
domain: nsError.domain,
|
|
158
|
+
code: nsError.code,
|
|
159
|
+
userInfo: [NSLocalizedDescriptionKey: describeError(nsError)]
|
|
160
|
+
))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Build a diagnostic string from an NSError that includes the domain,
|
|
164
|
+
/// code, localized description, and the full chain of underlying errors.
|
|
165
|
+
/// Used because `SPTError` (and many `URLSession` errors it wraps) often
|
|
166
|
+
/// have an empty `localizedDescription`, surfacing as "undefined reason"
|
|
167
|
+
/// in JS without this expansion.
|
|
168
|
+
private func describeError(_ error: NSError) -> String {
|
|
169
|
+
var parts: [String] = []
|
|
170
|
+
parts.append("\(error.domain) code \(error.code)")
|
|
171
|
+
let desc = error.localizedDescription
|
|
172
|
+
if !desc.isEmpty {
|
|
173
|
+
parts.append("\"\(desc)\"")
|
|
174
|
+
}
|
|
175
|
+
var ui = error.userInfo
|
|
176
|
+
ui.removeValue(forKey: NSUnderlyingErrorKey)
|
|
177
|
+
ui.removeValue(forKey: NSLocalizedDescriptionKey)
|
|
178
|
+
if !ui.isEmpty {
|
|
179
|
+
parts.append("userInfo=\(ui)")
|
|
180
|
+
}
|
|
181
|
+
if let underlying = error.userInfo[NSUnderlyingErrorKey] as? NSError {
|
|
182
|
+
parts.append("→ underlying: \(describeError(underlying))")
|
|
183
|
+
}
|
|
184
|
+
return parts.joined(separator: " ")
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -51,7 +51,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
51
51
|
- (instancetype)initWithClientID:(NSString *)clientID redirectURL:(NSURL *)redirectURL NS_DESIGNATED_INITIALIZER;
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
Convenience
|
|
54
|
+
Convenience initializer for `SPTConfiguration`
|
|
55
55
|
|
|
56
56
|
@param clientID Your client ID obtained from developer.spotify.com
|
|
57
57
|
@param redirectURL Your redirect URL for Spotify to open your app again after authorization
|
package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTError.h
CHANGED
|
@@ -10,13 +10,12 @@ SPT_EXPORT NSErrorDomain const SPTLoginErrorDomain;
|
|
|
10
10
|
typedef NS_ENUM(NSUInteger, SPTErrorCode) {
|
|
11
11
|
/// Unknown error code
|
|
12
12
|
SPTUnknownErrorCode NS_SWIFT_NAME(unknown) = 0,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
13
|
+
/// Authorization failed
|
|
14
|
+
SPTAuthorizationFailedErrorCode NS_SWIFT_NAME(authorizationFailed),
|
|
15
|
+
/// Renew session failed
|
|
16
|
+
SPTRenewSessionFailedErrorCode NS_SWIFT_NAME(renewSessionFailed),
|
|
17
|
+
/// Failed to parse the returned JSON
|
|
18
|
+
SPTJSONFailedErrorCode NS_SWIFT_NAME(jsonFailed) };
|
|
20
19
|
|
|
21
20
|
/// Spotify-specific errors. Use NSUnderlyingErrorKey to see the underlying error
|
|
22
21
|
@interface SPTError : NSError
|
package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTScope.h
CHANGED
|
@@ -30,42 +30,46 @@
|
|
|
30
30
|
typedef NS_OPTIONS(NSUInteger, SPTScope) {
|
|
31
31
|
/// Read access to user's private playlists.
|
|
32
32
|
SPTPlaylistReadPrivateScope NS_SWIFT_NAME(playlistReadPrivate) = (1 << 0),
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
33
|
+
/// Include collaborative playlists when requesting a user's playlists.
|
|
34
|
+
SPTPlaylistReadCollaborativeScope NS_SWIFT_NAME(playlistReadCollaborative) = (1 << 1),
|
|
35
|
+
/// Write access to a user's public playlists.
|
|
36
|
+
SPTPlaylistModifyPublicScope NS_SWIFT_NAME(playlistModifyPublic) = (1 << 2),
|
|
37
|
+
/// Write access to a user's private playlists.
|
|
38
|
+
SPTPlaylistModifyPrivateScope NS_SWIFT_NAME(playlistModifyPrivate) = (1 << 3),
|
|
39
|
+
/// Read access to the list of artists and other users that the user follows.
|
|
40
|
+
SPTUserFollowReadScope NS_SWIFT_NAME(userFollowRead) = (1 << 4),
|
|
41
|
+
/// Write/delete access to the list of artists and other users that the user follows.
|
|
42
|
+
SPTUserFollowModifyScope NS_SWIFT_NAME(userFollowModify) = (1 << 5),
|
|
43
|
+
/// Read access to a user's "Your Music" library.
|
|
44
|
+
SPTUserLibraryReadScope NS_SWIFT_NAME(userLibraryRead) = (1 << 6),
|
|
45
|
+
/// Write/delete access to a user's "Your Music" library.
|
|
46
|
+
SPTUserLibraryModifyScope NS_SWIFT_NAME(userLibraryModify) = (1 << 7),
|
|
47
|
+
/// Read access to the user's birthdate.
|
|
48
|
+
SPTUserReadBirthDateScope NS_SWIFT_NAME(userReadBirthDate) = (1 << 8),
|
|
49
|
+
/// Read access to user’s email address.
|
|
50
|
+
SPTUserReadEmailScope NS_SWIFT_NAME(userReadEmail) = (1 << 9),
|
|
51
|
+
/// Read access to user’s subscription details (type of user account).
|
|
52
|
+
SPTUserReadPrivateScope NS_SWIFT_NAME(userReadPrivate) = (1 << 10),
|
|
53
|
+
/// Read access to a user's top artists and tracks.
|
|
54
|
+
SPTUserTopReadScope NS_SWIFT_NAME(userTopRead) = (1 << 11),
|
|
55
|
+
/// Upload user generated content images
|
|
56
|
+
SPTUGCImageUploadScope NS_SWIFT_NAME(ugcImageUpload) = (1 << 12),
|
|
57
|
+
/// Control playback of a Spotify track.
|
|
58
|
+
SPTStreamingScope NS_SWIFT_NAME(streaming) = (1 << 13),
|
|
59
|
+
/// Use App Remote to control playback in the Spotify app
|
|
60
|
+
SPTAppRemoteControlScope NS_SWIFT_NAME(appRemoteControl) = (1 << 14),
|
|
61
|
+
/// Read access to a user’s player state.
|
|
62
|
+
SPTUserReadPlaybackStateScope NS_SWIFT_NAME(userReadPlaybackState) = (1 << 15),
|
|
63
|
+
/// Write access to a user’s playback state
|
|
64
|
+
SPTUserModifyPlaybackStateScope NS_SWIFT_NAME(userModifyPlaybackState) = (1 << 16),
|
|
65
|
+
/// Read access to a user’s currently playing
|
|
66
|
+
/// track
|
|
67
|
+
SPTUserReadCurrentlyPlayingScope NS_SWIFT_NAME(userReadCurrentlyPlaying) = (1 << 17),
|
|
68
|
+
/// Read access to a user’s currently
|
|
69
|
+
/// playing track
|
|
70
|
+
SPTUserReadRecentlyPlayedScope NS_SWIFT_NAME(userReadRecentlyPlayed) = (1 << 18),
|
|
71
|
+
/// Indicate that the application
|
|
72
|
+
/// intends to use OIDC to verify the
|
|
73
|
+
/// user's identity
|
|
74
|
+
SPTOpenIdScope NS_SWIFT_NAME(openid) = (1 << 19),
|
|
75
|
+
};
|
|
@@ -8,11 +8,15 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
8
8
|
|
|
9
9
|
/// Options for determining the most suitable method for authorization
|
|
10
10
|
typedef NS_OPTIONS(NSUInteger, SPTAuthorizationOptions) {
|
|
11
|
-
/// Authorize using a suitable method. If Spotify is installed the app will be used instead of
|
|
11
|
+
/// Authorize using a suitable method. If Spotify is installed the app will be used instead of ASWebAuthentication.
|
|
12
12
|
SPTDefaultAuthorizationOption NS_SWIFT_NAME(default) = (0),
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/// Authorize using only the Spotify client. If Spotify is not installed authorization will fail.
|
|
14
|
+
SPTClientAuthorizationOption NS_SWIFT_NAME(clientOnly) = (1 << 0),
|
|
15
|
+
/// Use this option if you have reached the limit of `LSApplicationQueriesSchemes` in your Info.plist
|
|
16
|
+
/// Attempt to open Spotify app directly without using `canOpenURL`. Falls back to ASWebAuthentication if
|
|
17
|
+
/// opening fails.
|
|
18
|
+
SPTSpotifySchemeNotRegisteredAuthorizationOption NS_SWIFT_NAME(spotifySchemeNotRegistered) = (1 << 1),
|
|
19
|
+
} NS_SWIFT_NAME(AuthorizationOptions);
|
|
16
20
|
|
|
17
21
|
typedef NSString *const SPTAuthorizationCode;
|
|
18
22
|
|
|
Binary file
|
|
Binary file
|
|
@@ -51,7 +51,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
51
51
|
- (instancetype)initWithClientID:(NSString *)clientID redirectURL:(NSURL *)redirectURL NS_DESIGNATED_INITIALIZER;
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
Convenience
|
|
54
|
+
Convenience initializer for `SPTConfiguration`
|
|
55
55
|
|
|
56
56
|
@param clientID Your client ID obtained from developer.spotify.com
|
|
57
57
|
@param redirectURL Your redirect URL for Spotify to open your app again after authorization
|
|
@@ -10,13 +10,12 @@ SPT_EXPORT NSErrorDomain const SPTLoginErrorDomain;
|
|
|
10
10
|
typedef NS_ENUM(NSUInteger, SPTErrorCode) {
|
|
11
11
|
/// Unknown error code
|
|
12
12
|
SPTUnknownErrorCode NS_SWIFT_NAME(unknown) = 0,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
13
|
+
/// Authorization failed
|
|
14
|
+
SPTAuthorizationFailedErrorCode NS_SWIFT_NAME(authorizationFailed),
|
|
15
|
+
/// Renew session failed
|
|
16
|
+
SPTRenewSessionFailedErrorCode NS_SWIFT_NAME(renewSessionFailed),
|
|
17
|
+
/// Failed to parse the returned JSON
|
|
18
|
+
SPTJSONFailedErrorCode NS_SWIFT_NAME(jsonFailed) };
|
|
20
19
|
|
|
21
20
|
/// Spotify-specific errors. Use NSUnderlyingErrorKey to see the underlying error
|
|
22
21
|
@interface SPTError : NSError
|