@wwdrew/expo-spotify-sdk 0.7.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +388 -108
- package/android/build.gradle +10 -0
- package/android/consumer-rules.pro +5 -0
- package/android/libs/SETUP.md +29 -0
- package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +149 -12
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteCoordinator.kt +556 -0
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyConfig.kt +1 -0
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyErrors.kt +112 -0
- package/build/ExpoSpotifySDK.types.d.ts +12 -0
- package/build/ExpoSpotifySDK.types.d.ts.map +1 -1
- package/build/ExpoSpotifySDK.types.js.map +1 -1
- package/build/ExpoSpotifySDKModule.web.d.ts +23 -0
- package/build/ExpoSpotifySDKModule.web.d.ts.map +1 -1
- package/build/ExpoSpotifySDKModule.web.js +74 -5
- package/build/ExpoSpotifySDKModule.web.js.map +1 -1
- package/build/app-remote/error.d.ts +8 -0
- package/build/app-remote/error.d.ts.map +1 -0
- package/build/app-remote/error.js +10 -0
- package/build/app-remote/error.js.map +1 -0
- package/build/app-remote/index.d.ts +79 -0
- package/build/app-remote/index.d.ts.map +1 -0
- package/build/app-remote/index.js +110 -0
- package/build/app-remote/index.js.map +1 -0
- package/build/auth/error.d.ts +8 -0
- package/build/auth/error.d.ts.map +1 -0
- package/build/auth/error.js +10 -0
- package/build/auth/error.js.map +1 -0
- package/build/auth/index.d.ts +124 -0
- package/build/auth/index.d.ts.map +1 -0
- package/build/auth/index.js +162 -0
- package/build/auth/index.js.map +1 -0
- package/build/content/error.d.ts +8 -0
- package/build/content/error.d.ts.map +1 -0
- package/build/content/error.js +10 -0
- package/build/content/error.js.map +1 -0
- package/build/content/index.d.ts +38 -0
- package/build/content/index.d.ts.map +1 -0
- package/build/content/index.js +56 -0
- package/build/content/index.js.map +1 -0
- package/build/error.d.ts +24 -0
- package/build/error.d.ts.map +1 -0
- package/build/error.js +25 -0
- package/build/error.js.map +1 -0
- package/build/hooks/index.d.ts +90 -0
- package/build/hooks/index.d.ts.map +1 -0
- package/build/hooks/index.js +326 -0
- package/build/hooks/index.js.map +1 -0
- package/build/images/error.d.ts +8 -0
- package/build/images/error.d.ts.map +1 -0
- package/build/images/error.js +10 -0
- package/build/images/error.js.map +1 -0
- package/build/images/index.d.ts +36 -0
- package/build/images/index.d.ts.map +1 -0
- package/build/images/index.js +59 -0
- package/build/images/index.js.map +1 -0
- package/build/index.d.ts +44 -42
- package/build/index.d.ts.map +1 -1
- package/build/index.js +49 -122
- package/build/index.js.map +1 -1
- package/build/player/error.d.ts +8 -0
- package/build/player/error.d.ts.map +1 -0
- package/build/player/error.js +10 -0
- package/build/player/error.js.map +1 -0
- package/build/player/index.d.ts +140 -0
- package/build/player/index.d.ts.map +1 -0
- package/build/player/index.js +136 -0
- package/build/player/index.js.map +1 -0
- package/build/uri/index.d.ts +55 -0
- package/build/uri/index.d.ts.map +1 -0
- package/build/uri/index.js +57 -0
- package/build/uri/index.js.map +1 -0
- package/build/user/error.d.ts +8 -0
- package/build/user/error.d.ts.map +1 -0
- package/build/user/error.js +10 -0
- package/build/user/error.js.map +1 -0
- package/build/user/index.d.ts +44 -0
- package/build/user/index.d.ts.map +1 -0
- package/build/user/index.js +97 -0
- package/build/user/index.js.map +1 -0
- package/ios/ExpoSpotifySDKModule.swift +233 -6
- package/ios/SPTScopeSerializer.swift +5 -0
- package/ios/SpotifyAppRemoteCoordinator.swift +939 -0
- package/ios/SpotifyAuthCoordinator.swift +67 -13
- package/ios/SpotifyTokenRefreshClient.swift +32 -1
- package/package.json +4 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
1
2
|
import Foundation
|
|
2
3
|
import SpotifyiOS
|
|
3
4
|
|
|
@@ -9,7 +10,7 @@ enum SpotifyError: Error {
|
|
|
9
10
|
case authInProgress
|
|
10
11
|
case userCancelled
|
|
11
12
|
case spotifyNotInstalled
|
|
12
|
-
case underlying(Error)
|
|
13
|
+
case underlying(message: String, cause: Error)
|
|
13
14
|
|
|
14
15
|
var code: String {
|
|
15
16
|
switch self {
|
|
@@ -23,13 +24,45 @@ enum SpotifyError: Error {
|
|
|
23
24
|
|
|
24
25
|
var message: String {
|
|
25
26
|
switch self {
|
|
26
|
-
case .invalidConfiguration(let m):
|
|
27
|
-
case .authInProgress:
|
|
28
|
-
case .userCancelled:
|
|
29
|
-
case .spotifyNotInstalled:
|
|
30
|
-
case .underlying(let
|
|
27
|
+
case .invalidConfiguration(let m): return m
|
|
28
|
+
case .authInProgress: return "Another authentication request is already in progress"
|
|
29
|
+
case .userCancelled: return "Authentication was cancelled by the user"
|
|
30
|
+
case .spotifyNotInstalled: return "The Spotify app is not installed on this device"
|
|
31
|
+
case .underlying(let message, _): return message
|
|
31
32
|
}
|
|
32
33
|
}
|
|
34
|
+
|
|
35
|
+
/// The original error that caused this failure, if any. Surfaced as the
|
|
36
|
+
/// JS-facing exception's `cause` so debugging / Sentry breadcrumbs keep the
|
|
37
|
+
/// full chain rather than collapsing into "undefined reason".
|
|
38
|
+
var underlyingCause: Error? {
|
|
39
|
+
switch self {
|
|
40
|
+
case .underlying(_, let cause): return cause
|
|
41
|
+
default: return nil
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// `Exception` subclass that projects a `SpotifyError`'s `code` and `message`
|
|
47
|
+
/// through `expo-modules-core`'s exception bridge so JS receives the structured
|
|
48
|
+
/// code and a meaningful reason — not the default "undefined reason" placeholder.
|
|
49
|
+
///
|
|
50
|
+
/// Without this wrapper an `AsyncFunction` rejection collapses to
|
|
51
|
+
/// `FunctionCallException` → `cause.reason = "undefined reason"` because the
|
|
52
|
+
/// previously-used `GenericException<String>` does not override `reason`.
|
|
53
|
+
final class SpotifyAuthException: Exception, @unchecked Sendable {
|
|
54
|
+
private let spotifyCode: String
|
|
55
|
+
private let spotifyMessage: String
|
|
56
|
+
|
|
57
|
+
init(_ error: SpotifyError, file: String = #fileID, line: UInt = #line, function: String = #function) {
|
|
58
|
+
self.spotifyCode = error.code
|
|
59
|
+
self.spotifyMessage = error.message
|
|
60
|
+
super.init(file: file, line: line, function: function)
|
|
61
|
+
self.cause = error.underlyingCause
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override var code: String { spotifyCode }
|
|
65
|
+
override var reason: String { spotifyMessage }
|
|
33
66
|
}
|
|
34
67
|
|
|
35
68
|
/// `actor` ensures `pending` is mutated only on the actor's serial executor;
|
|
@@ -38,7 +71,7 @@ actor SpotifyAuthCoordinator {
|
|
|
38
71
|
static let shared: SpotifyAuthCoordinator? = SpotifyAuthCoordinator.create()
|
|
39
72
|
|
|
40
73
|
private let sptConfiguration: SPTConfiguration
|
|
41
|
-
private let sessionManager: SPTSessionManager
|
|
74
|
+
nonisolated(unsafe) private let sessionManager: SPTSessionManager
|
|
42
75
|
private let bridge: SpotifySessionDelegateBridge
|
|
43
76
|
private var pending: CheckedContinuation<SPTSession, Error>?
|
|
44
77
|
|
|
@@ -72,7 +105,15 @@ actor SpotifyAuthCoordinator {
|
|
|
72
105
|
/// Synchronous URL-handler dispatcher used by the AppDelegate / SceneDelegate
|
|
73
106
|
/// subscribers to forward redirects to the SPTSessionManager.
|
|
74
107
|
nonisolated func handleOpenURL(_ url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
|
|
75
|
-
|
|
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()
|
|
115
|
+
}
|
|
116
|
+
return DispatchQueue.main.sync(execute: openSession)
|
|
76
117
|
}
|
|
77
118
|
|
|
78
119
|
func authenticate(
|
|
@@ -112,6 +153,20 @@ actor SpotifyAuthCoordinator {
|
|
|
112
153
|
pending = nil
|
|
113
154
|
cont?.resume(with: result)
|
|
114
155
|
}
|
|
156
|
+
|
|
157
|
+
/// Forcibly cancel any in-flight authenticate() call. The pending
|
|
158
|
+
/// continuation (if any) is resumed with `userCancelled` and `pending` is
|
|
159
|
+
/// cleared. Safe to call when nothing is in flight (no-op).
|
|
160
|
+
///
|
|
161
|
+
/// Needed because the SPTSessionManager delegate callbacks are not
|
|
162
|
+
/// guaranteed to fire — e.g. when Spotify never redirects back to the host
|
|
163
|
+
/// app — leaving `pending` set forever and every subsequent authenticate()
|
|
164
|
+
/// rejecting with `authInProgress`.
|
|
165
|
+
func cancelPending() {
|
|
166
|
+
guard let cont = pending else { return }
|
|
167
|
+
pending = nil
|
|
168
|
+
cont.resume(throwing: SpotifyError.userCancelled)
|
|
169
|
+
}
|
|
115
170
|
}
|
|
116
171
|
|
|
117
172
|
/// `SPTSessionManagerDelegate` requires NSObject conformance, which an actor
|
|
@@ -153,11 +208,10 @@ final class SpotifySessionDelegateBridge: NSObject, SPTSessionManagerDelegate {
|
|
|
153
208
|
if description.contains("cancel") {
|
|
154
209
|
return SpotifyError.userCancelled
|
|
155
210
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
))
|
|
211
|
+
// Keep the original NSError as `cause` so the structured underlying-chain
|
|
212
|
+
// is preserved (Sentry, debug breadcrumbs); the rendered string goes to
|
|
213
|
+
// `message` so JS callers get a single human-readable line.
|
|
214
|
+
return SpotifyError.underlying(message: describeError(nsError), cause: nsError)
|
|
161
215
|
}
|
|
162
216
|
|
|
163
217
|
/// Build a diagnostic string from an NSError that includes the domain,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
1
2
|
import Foundation
|
|
2
3
|
|
|
3
4
|
enum SpotifyRefreshError: Error {
|
|
@@ -28,6 +29,33 @@ enum SpotifyRefreshError: Error {
|
|
|
28
29
|
return m
|
|
29
30
|
}
|
|
30
31
|
}
|
|
32
|
+
|
|
33
|
+
/// The original error that caused this failure, if any. Surfaced as the
|
|
34
|
+
/// JS-facing exception's `cause`.
|
|
35
|
+
var underlyingCause: Error? {
|
|
36
|
+
switch self {
|
|
37
|
+
case .network(let err): return err
|
|
38
|
+
default: return nil
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// `Exception` subclass that projects a `SpotifyRefreshError`'s `code` and
|
|
44
|
+
/// `message` through `expo-modules-core` so JS sees the structured taxonomy
|
|
45
|
+
/// instead of "undefined reason". Mirrors `SpotifyAuthException`.
|
|
46
|
+
final class SpotifyRefreshException: Exception, @unchecked Sendable {
|
|
47
|
+
private let spotifyCode: String
|
|
48
|
+
private let spotifyMessage: String
|
|
49
|
+
|
|
50
|
+
init(_ error: SpotifyRefreshError, file: String = #fileID, line: UInt = #line, function: String = #function) {
|
|
51
|
+
self.spotifyCode = error.code
|
|
52
|
+
self.spotifyMessage = error.message
|
|
53
|
+
super.init(file: file, line: line, function: function)
|
|
54
|
+
self.cause = error.underlyingCause
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override var code: String { spotifyCode }
|
|
58
|
+
override var reason: String { spotifyMessage }
|
|
31
59
|
}
|
|
32
60
|
|
|
33
61
|
struct SpotifyRefreshResult {
|
|
@@ -113,6 +141,9 @@ struct SpotifyTokenRefreshClient {
|
|
|
113
141
|
private func formURLEncoded(_ pairs: [String: String]) -> String {
|
|
114
142
|
var components = URLComponents()
|
|
115
143
|
components.queryItems = pairs.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
116
|
-
|
|
144
|
+
// `URLComponents.percentEncodedQuery` does NOT escape `+`, but in
|
|
145
|
+
// `application/x-www-form-urlencoded` bodies `+` means space. Replace it
|
|
146
|
+
// explicitly so refresh tokens containing `+` survive the round-trip.
|
|
147
|
+
return (components.percentEncodedQuery ?? "").replacingOccurrences(of: "+", with: "%2B")
|
|
117
148
|
}
|
|
118
149
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wwdrew/expo-spotify-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Expo module wrapping the native Spotify iOS (v5) and Android (v4) SDKs for OAuth authentication",
|
|
3
|
+
"version": "1.0.0",
|
|
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",
|
|
7
7
|
"scripts": {
|
|
@@ -51,5 +51,6 @@
|
|
|
51
51
|
"expo": "*",
|
|
52
52
|
"react": "*",
|
|
53
53
|
"react-native": "*"
|
|
54
|
-
}
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {}
|
|
55
56
|
}
|