@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.
@@ -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.0" // x-release-please-version
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.0" // x-release-please-version
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 — %@", String(describing: error))
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 — %@", String(describing: error))
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.localizedDescription
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(error.localizedDescription)
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 desc = error.localizedDescription.lowercased()
41
- if desc.contains("uri") {
42
- return .invalidURI("\(callsite): \(error.localizedDescription)")
41
+ let lower = desc.lowercased()
42
+ if lower.contains("uri") {
43
+ return .invalidURI("\(callsite): \(desc)")
43
44
  }
44
- return .invalidParameter(error.localizedDescription)
45
+ return .invalidParameter(desc)
45
46
  }
46
47
  if isRequestFailed(error) {
47
- let desc = error.localizedDescription.lowercased()
48
- if desc.contains("premium") {
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(error.localizedDescription) {
52
- return .operationNotAllowed("\(callsite): \(error.localizedDescription)")
52
+ if containsRestriction(desc) {
53
+ return .operationNotAllowed("\(callsite): \(desc)")
53
54
  }
54
- return .unknown(error.localizedDescription)
55
+ return .unknown(desc)
55
56
  }
56
- return .unknown(error.localizedDescription)
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(error.localizedDescription)
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): \(error.localizedDescription)")
69
+ return .invalidURI("\(callsite): \(desc)")
68
70
  }
69
71
  if isRequestFailed(error) {
70
- if containsRestriction(error.localizedDescription) {
71
- return .operationNotAllowed("\(callsite): \(error.localizedDescription)")
72
+ if containsRestriction(desc) {
73
+ return .operationNotAllowed("\(callsite): \(desc)")
72
74
  }
73
- return .unknown(error.localizedDescription)
75
+ return .unknown(desc)
74
76
  }
75
- return .unknown(error.localizedDescription)
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(error.localizedDescription)
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 desc = error.localizedDescription.lowercased()
87
- if desc.contains("not supported") || desc.contains("unsupported") {
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(error.localizedDescription)
93
+ return .unknown(desc)
91
94
  }
92
- return .unknown(error.localizedDescription)
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(error.localizedDescription)
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(error.localizedDescription)
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 %@", String(describing: error))
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.localizedDescription,
180
- error.localizedFailureReason ?? "",
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.localizedDescription
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.localizedDescription
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("Refresh response was not valid JSON: \(error.localizedDescription)")
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.0",
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",