@wwdrew/expo-spotify-sdk 2.3.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +1 -1
- package/ios/ExpoSpotifySDKModule.swift +1 -1
- package/ios/NSError+SafeLocalizedDescription.swift +40 -0
- package/ios/SpotifyAppRemoteConnection.swift +2 -2
- package/ios/SpotifyAppRemoteCoordinator.swift +1 -1
- package/ios/SpotifyAppRemoteErrorMapping.swift +28 -24
- package/ios/SpotifyAuthCoordinator.swift +1 -1
- package/ios/SpotifyAuthErrorMapping.swift +3 -5
- package/ios/SpotifyTokenRefreshClient.swift +4 -2
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ import expo.modules.kotlin.functions.Coroutine
|
|
|
8
8
|
import expo.modules.kotlin.modules.Module
|
|
9
9
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
10
10
|
|
|
11
|
-
private const val SDK_VERSION = "2.3.
|
|
11
|
+
private const val SDK_VERSION = "2.3.1" // x-release-please-version
|
|
12
12
|
private const val EVENT_SESSION_CHANGE = "onSessionChange"
|
|
13
13
|
private const val EVENT_CONNECTION_STATE_CHANGE = "onConnectionStateChange"
|
|
14
14
|
private const val EVENT_CONNECTION_ERROR = "onConnectionError"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
2
|
import SpotifyiOS
|
|
3
3
|
|
|
4
|
-
private let SDK_VERSION = "2.3.
|
|
4
|
+
private let SDK_VERSION = "2.3.1" // x-release-please-version
|
|
5
5
|
private let EVENT_SESSION_CHANGE = "onSessionChange"
|
|
6
6
|
private let EVENT_CONNECTION_STATE_CHANGE = "onConnectionStateChange"
|
|
7
7
|
private let EVENT_CONNECTION_ERROR = "onConnectionError"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
extension NSError {
|
|
4
|
+
/// `NSError.localizedDescription` force-bridges `userInfo[NSLocalizedDescriptionKey]`
|
|
5
|
+
/// to `String`. When that value is a non-`String` object (Spotify's SDK can deliver an
|
|
6
|
+
/// empty `__NSDictionary0`), the bridge sends `-length` to a dictionary and aborts.
|
|
7
|
+
var safeLocalizedDescription: String {
|
|
8
|
+
guard let value = userInfo[NSLocalizedDescriptionKey] else {
|
|
9
|
+
return localizedDescription
|
|
10
|
+
}
|
|
11
|
+
return (value as? String) ?? ""
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// Same force-bridge hazard as `localizedDescription`, but for
|
|
15
|
+
/// `NSLocalizedFailureReasonErrorKey`.
|
|
16
|
+
var safeLocalizedFailureReason: String? {
|
|
17
|
+
guard let value = userInfo[NSLocalizedFailureReasonErrorKey] else {
|
|
18
|
+
return localizedFailureReason
|
|
19
|
+
}
|
|
20
|
+
return value as? String
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
extension Error {
|
|
25
|
+
/// Summary for logging that avoids `String(describing:)` / `localizedDescription`
|
|
26
|
+
/// force-bridges on poisoned `userInfo` values.
|
|
27
|
+
var safeLogSummary: String {
|
|
28
|
+
let nsError = self as NSError
|
|
29
|
+
let description = nsError.safeLocalizedDescription
|
|
30
|
+
if description.isEmpty {
|
|
31
|
+
return "\(nsError.domain) code \(nsError.code)"
|
|
32
|
+
}
|
|
33
|
+
return "\(nsError.domain) code \(nsError.code): \(description)"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func safeLogSummary(for error: (any Error)?) -> String {
|
|
38
|
+
guard let error else { return "nil" }
|
|
39
|
+
return error.safeLogSummary
|
|
40
|
+
}
|
|
@@ -210,12 +210,12 @@ final class SpotifyAppRemoteDelegateBridge: NSObject, SPTAppRemoteDelegate {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
func appRemote(_ appRemote: SPTAppRemote, didFailConnectionAttemptWithError error: (any Error)?) {
|
|
213
|
-
NSLog("[ExpoSpotifySDK] AppRemote: connection failed — %@",
|
|
213
|
+
NSLog("[ExpoSpotifySDK] AppRemote: connection failed — %@", safeLogSummary(for: error))
|
|
214
214
|
Task { await coordinator?.didFailToConnect(error: error) }
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
func appRemote(_ appRemote: SPTAppRemote, didDisconnectWithError error: (any Error)?) {
|
|
218
|
-
NSLog("[ExpoSpotifySDK] AppRemote: disconnected — %@",
|
|
218
|
+
NSLog("[ExpoSpotifySDK] AppRemote: disconnected — %@", safeLogSummary(for: error))
|
|
219
219
|
Task { await coordinator?.didDisconnect(error: error) }
|
|
220
220
|
}
|
|
221
221
|
}
|
|
@@ -421,7 +421,7 @@ actor SpotifyAppRemoteCoordinator {
|
|
|
421
421
|
|
|
422
422
|
func describeNSError(_ error: NSError) -> String {
|
|
423
423
|
var parts: [String] = ["\(error.domain) code \(error.code)"]
|
|
424
|
-
let desc = error.
|
|
424
|
+
let desc = error.safeLocalizedDescription
|
|
425
425
|
if !desc.isEmpty { parts.append("\"\(desc)\"") }
|
|
426
426
|
if let underlying = error.userInfo[NSUnderlyingErrorKey] as? NSError {
|
|
427
427
|
parts.append("→ \(describeNSError(underlying))")
|
|
@@ -30,71 +30,75 @@ enum SpotifyAppRemoteErrorMapping {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
static func mapPlayerError(_ error: NSError, callsite: String) -> NativePlayerError {
|
|
33
|
+
let desc = error.safeLocalizedDescription
|
|
33
34
|
guard isSPTError(error) else {
|
|
34
|
-
return .unknown(
|
|
35
|
+
return .unknown(desc)
|
|
35
36
|
}
|
|
36
37
|
if isConnectionTerminated(error) {
|
|
37
38
|
return .connectionLost(connectionLostMessage(callsite: callsite))
|
|
38
39
|
}
|
|
39
40
|
if isInvalidArguments(error) {
|
|
40
|
-
let
|
|
41
|
-
if
|
|
42
|
-
return .invalidURI("\(callsite): \(
|
|
41
|
+
let lower = desc.lowercased()
|
|
42
|
+
if lower.contains("uri") {
|
|
43
|
+
return .invalidURI("\(callsite): \(desc)")
|
|
43
44
|
}
|
|
44
|
-
return .invalidParameter(
|
|
45
|
+
return .invalidParameter(desc)
|
|
45
46
|
}
|
|
46
47
|
if isRequestFailed(error) {
|
|
47
|
-
let
|
|
48
|
-
if
|
|
48
|
+
let lower = desc.lowercased()
|
|
49
|
+
if lower.contains("premium") {
|
|
49
50
|
return .premiumRequired("\(callsite): Spotify Premium is required for on-demand playback")
|
|
50
51
|
}
|
|
51
|
-
if containsRestriction(
|
|
52
|
-
return .operationNotAllowed("\(callsite): \(
|
|
52
|
+
if containsRestriction(desc) {
|
|
53
|
+
return .operationNotAllowed("\(callsite): \(desc)")
|
|
53
54
|
}
|
|
54
|
-
return .unknown(
|
|
55
|
+
return .unknown(desc)
|
|
55
56
|
}
|
|
56
|
-
return .unknown(
|
|
57
|
+
return .unknown(desc)
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
static func mapUserError(_ error: NSError, callsite: String) -> NativeUserError {
|
|
61
|
+
let desc = error.safeLocalizedDescription
|
|
60
62
|
guard isSPTError(error) else {
|
|
61
|
-
return .unknown(
|
|
63
|
+
return .unknown(desc)
|
|
62
64
|
}
|
|
63
65
|
if isConnectionTerminated(error) {
|
|
64
66
|
return .connectionLost(connectionLostMessage(callsite: callsite))
|
|
65
67
|
}
|
|
66
68
|
if isInvalidArguments(error) {
|
|
67
|
-
return .invalidURI("\(callsite): \(
|
|
69
|
+
return .invalidURI("\(callsite): \(desc)")
|
|
68
70
|
}
|
|
69
71
|
if isRequestFailed(error) {
|
|
70
|
-
if containsRestriction(
|
|
71
|
-
return .operationNotAllowed("\(callsite): \(
|
|
72
|
+
if containsRestriction(desc) {
|
|
73
|
+
return .operationNotAllowed("\(callsite): \(desc)")
|
|
72
74
|
}
|
|
73
|
-
return .unknown(
|
|
75
|
+
return .unknown(desc)
|
|
74
76
|
}
|
|
75
|
-
return .unknown(
|
|
77
|
+
return .unknown(desc)
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
static func mapContentError(_ error: NSError, callsite: String) -> NativeContentError {
|
|
81
|
+
let desc = error.safeLocalizedDescription
|
|
79
82
|
guard isSPTError(error) else {
|
|
80
|
-
return .unknown(
|
|
83
|
+
return .unknown(desc)
|
|
81
84
|
}
|
|
82
85
|
if isConnectionTerminated(error) {
|
|
83
86
|
return .connectionLost(connectionLostMessage(callsite: callsite))
|
|
84
87
|
}
|
|
85
88
|
if isRequestFailed(error) {
|
|
86
|
-
let
|
|
87
|
-
if
|
|
89
|
+
let lower = desc.lowercased()
|
|
90
|
+
if lower.contains("not supported") || lower.contains("unsupported") {
|
|
88
91
|
return .contentAPIUnavailable("\(callsite): content API is unavailable on this Spotify app version")
|
|
89
92
|
}
|
|
90
|
-
return .unknown(
|
|
93
|
+
return .unknown(desc)
|
|
91
94
|
}
|
|
92
|
-
return .unknown(
|
|
95
|
+
return .unknown(desc)
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
static func mapImagesError(_ error: NSError, callsite: String) -> NativeImagesError {
|
|
99
|
+
let desc = error.safeLocalizedDescription
|
|
96
100
|
guard isSPTError(error) else {
|
|
97
|
-
return .unknown(
|
|
101
|
+
return .unknown(desc)
|
|
98
102
|
}
|
|
99
103
|
if isConnectionTerminated(error) {
|
|
100
104
|
return .notConnected(connectionLostMessage(callsite: callsite))
|
|
@@ -105,6 +109,6 @@ enum SpotifyAppRemoteErrorMapping {
|
|
|
105
109
|
if isRequestFailed(error) {
|
|
106
110
|
return .imageLoadFailed("\(callsite): Spotify rejected image request")
|
|
107
111
|
}
|
|
108
|
-
return .unknown(
|
|
112
|
+
return .unknown(desc)
|
|
109
113
|
}
|
|
110
114
|
}
|
|
@@ -138,7 +138,7 @@ final class SpotifySessionDelegateBridge: NSObject, SPTSessionManagerDelegate {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
func sessionManager(manager _: SPTSessionManager, didFailWith error: Error) {
|
|
141
|
-
NSLog("[ExpoSpotifySDK] didFailWithError %@",
|
|
141
|
+
NSLog("[ExpoSpotifySDK] didFailWithError %@", error.safeLogSummary)
|
|
142
142
|
let mapped = SpotifyAuthErrorMapping.classify(
|
|
143
143
|
error,
|
|
144
144
|
context: .init(tokenSwapConfigured: authContext?.tokenSwapURL != nil)
|
|
@@ -176,10 +176,8 @@ enum SpotifyAuthErrorMapping {
|
|
|
176
176
|
"server", "unauthorized", "forbidden", "invalid"
|
|
177
177
|
]
|
|
178
178
|
let combined = [
|
|
179
|
-
error.
|
|
180
|
-
error.
|
|
181
|
-
error.userInfo[NSLocalizedDescriptionKey] as? String ?? "",
|
|
182
|
-
error.userInfo[NSLocalizedFailureReasonErrorKey] as? String ?? ""
|
|
179
|
+
error.safeLocalizedDescription,
|
|
180
|
+
error.safeLocalizedFailureReason ?? ""
|
|
183
181
|
].joined(separator: " ").lowercased()
|
|
184
182
|
|
|
185
183
|
if negativeKeywords.contains(where: { combined.contains($0) }) {
|
|
@@ -212,7 +210,7 @@ enum SpotifyAuthErrorMapping {
|
|
|
212
210
|
/// in-process classification heuristics only.
|
|
213
211
|
private static func describeError(_ error: NSError, redactUserInfo: Bool = false) -> String {
|
|
214
212
|
var parts: [String] = ["\(error.domain) code \(error.code)"]
|
|
215
|
-
let desc = error.
|
|
213
|
+
let desc = error.safeLocalizedDescription
|
|
216
214
|
if !desc.isEmpty {
|
|
217
215
|
parts.append("\"\(desc)\"")
|
|
218
216
|
}
|
|
@@ -23,7 +23,7 @@ enum SpotifyRefreshError: Error {
|
|
|
23
23
|
case .invalidURL(let s):
|
|
24
24
|
return "Invalid token refresh URL: \(s)"
|
|
25
25
|
case .network(let err):
|
|
26
|
-
return err.
|
|
26
|
+
return (err as NSError).safeLocalizedDescription
|
|
27
27
|
case .http(let status, let body):
|
|
28
28
|
let trimmed = body.map { String($0.prefix(512)) } ?? ""
|
|
29
29
|
return "Token refresh server returned HTTP \(status)\(trimmed.isEmpty ? "" : ": \(trimmed)")"
|
|
@@ -127,7 +127,9 @@ struct SpotifyTokenRefreshClient {
|
|
|
127
127
|
}
|
|
128
128
|
json = parsed
|
|
129
129
|
} catch {
|
|
130
|
-
throw SpotifyRefreshError.parse(
|
|
130
|
+
throw SpotifyRefreshError.parse(
|
|
131
|
+
"Refresh response was not valid JSON: \((error as NSError).safeLocalizedDescription)"
|
|
132
|
+
)
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
guard let accessToken = (json["access_token"] as? String), !accessToken.isEmpty else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wwdrew/expo-spotify-sdk",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.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",
|