@wwdrew/expo-spotify-sdk 2.2.2 → 2.2.3
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 +1 -0
- package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +15 -1
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyTokenSwapClient.kt +7 -2
- package/ios/ExpoSpotifySDKModule.swift +1 -1
- package/ios/SpotifyAuthCoordinator.swift +244 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -406,6 +406,7 @@ Need the deprecated shims temporarily? Pin **`1.x`**: `npm install @wwdrew/expo-
|
|
|
406
406
|
| --- | --- |
|
|
407
407
|
| [API reference](./docs/api-reference.md) | All namespaces, methods, hooks |
|
|
408
408
|
| [Error codes](./docs/error-codes.md) | Per-namespace codes with when/what-to-do |
|
|
409
|
+
| [Auth error mapping](./docs/auth-error-mapping.md) | iOS/Android auth → JS mapping matrix and parity checklist |
|
|
409
410
|
| [App Remote error mapping](./docs/app-remote-error-mapping.md) | iOS/Android native → JS mapping matrix |
|
|
410
411
|
| [Platform differences](./docs/guides/platform-differences.md) | iOS vs Android parity |
|
|
411
412
|
| [Native SDK distribution](./docs/guides/native-sdk-distribution.md) | How iOS/Android binaries are fetched, packaged, and bumped |
|
|
@@ -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.2.
|
|
11
|
+
private const val SDK_VERSION = "2.2.3" // 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"
|
|
@@ -165,6 +165,13 @@ class ExpoSpotifySDKModule : Module() {
|
|
|
165
165
|
} catch (e: expo.modules.kotlin.exception.CodedException) {
|
|
166
166
|
emitSessionError(e.code ?: "UNKNOWN", e.localizedMessage ?: e.code ?: "Unknown error")
|
|
167
167
|
throw e
|
|
168
|
+
} catch (e: Exception) {
|
|
169
|
+
val wrapped = UnknownSpotifyException(
|
|
170
|
+
"Unexpected authentication error: ${e.message ?: e::class.java.simpleName}",
|
|
171
|
+
e,
|
|
172
|
+
)
|
|
173
|
+
emitSessionError(wrapped.code ?: "UNKNOWN", wrapped.localizedMessage ?: "Unknown error")
|
|
174
|
+
throw wrapped
|
|
168
175
|
}
|
|
169
176
|
}
|
|
170
177
|
|
|
@@ -187,6 +194,13 @@ class ExpoSpotifySDKModule : Module() {
|
|
|
187
194
|
} catch (e: expo.modules.kotlin.exception.CodedException) {
|
|
188
195
|
emitSessionError(e.code ?: "UNKNOWN", e.localizedMessage ?: e.code ?: "Unknown error")
|
|
189
196
|
throw e
|
|
197
|
+
} catch (e: Exception) {
|
|
198
|
+
val wrapped = UnknownSpotifyException(
|
|
199
|
+
"Unexpected token refresh error: ${e.message ?: e::class.java.simpleName}",
|
|
200
|
+
e,
|
|
201
|
+
)
|
|
202
|
+
emitSessionError(wrapped.code ?: "UNKNOWN", wrapped.localizedMessage ?: "Unknown error")
|
|
203
|
+
throw wrapped
|
|
190
204
|
}
|
|
191
205
|
}
|
|
192
206
|
|
|
@@ -4,6 +4,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
|
|
4
4
|
import okhttp3.Call
|
|
5
5
|
import okhttp3.Callback
|
|
6
6
|
import okhttp3.FormBody
|
|
7
|
+
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
7
8
|
import okhttp3.OkHttpClient
|
|
8
9
|
import okhttp3.Request
|
|
9
10
|
import okhttp3.Response
|
|
@@ -43,11 +44,13 @@ class SpotifyTokenSwapClient(
|
|
|
43
44
|
tokenSwapURL: String,
|
|
44
45
|
requestedScopes: List<String>,
|
|
45
46
|
): SpotifySessionPayload {
|
|
47
|
+
val url = tokenSwapURL.toHttpUrlOrNull()
|
|
48
|
+
?: throw InvalidConfigException("Invalid token swap URL: $tokenSwapURL")
|
|
46
49
|
val body = FormBody.Builder()
|
|
47
50
|
.add("code", code)
|
|
48
51
|
.build()
|
|
49
52
|
val request = Request.Builder()
|
|
50
|
-
.url(
|
|
53
|
+
.url(url)
|
|
51
54
|
.header("User-Agent", userAgent)
|
|
52
55
|
.post(body)
|
|
53
56
|
.build()
|
|
@@ -60,11 +63,13 @@ class SpotifyTokenSwapClient(
|
|
|
60
63
|
tokenRefreshURL: String,
|
|
61
64
|
previousScopes: List<String>,
|
|
62
65
|
): SpotifySessionPayload {
|
|
66
|
+
val url = tokenRefreshURL.toHttpUrlOrNull()
|
|
67
|
+
?: throw InvalidConfigException("Invalid token refresh URL: $tokenRefreshURL")
|
|
63
68
|
val body = FormBody.Builder()
|
|
64
69
|
.add("refresh_token", refreshToken)
|
|
65
70
|
.build()
|
|
66
71
|
val request = Request.Builder()
|
|
67
|
-
.url(
|
|
72
|
+
.url(url)
|
|
68
73
|
.header("User-Agent", userAgent)
|
|
69
74
|
.post(body)
|
|
70
75
|
.build()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
2
|
import SpotifyiOS
|
|
3
3
|
|
|
4
|
-
private let SDK_VERSION = "2.2.
|
|
4
|
+
private let SDK_VERSION = "2.2.3" // 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"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
|
+
import AuthenticationServices
|
|
2
3
|
import Foundation
|
|
3
4
|
import SpotifyiOS
|
|
4
5
|
import UIKit
|
|
@@ -11,6 +12,10 @@ enum SpotifyError: Error {
|
|
|
11
12
|
case authInProgress
|
|
12
13
|
case userCancelled
|
|
13
14
|
case spotifyNotInstalled
|
|
15
|
+
case networkError(message: String, cause: Error)
|
|
16
|
+
case tokenSwapFailed(status: Int?, message: String, cause: Error)
|
|
17
|
+
case tokenSwapParseError(message: String, cause: Error)
|
|
18
|
+
case authError(message: String, cause: Error)
|
|
14
19
|
case underlying(message: String, cause: Error)
|
|
15
20
|
|
|
16
21
|
var code: String {
|
|
@@ -19,6 +24,10 @@ enum SpotifyError: Error {
|
|
|
19
24
|
case .authInProgress: return "AUTH_IN_PROGRESS"
|
|
20
25
|
case .userCancelled: return "USER_CANCELLED"
|
|
21
26
|
case .spotifyNotInstalled: return "SPOTIFY_NOT_INSTALLED"
|
|
27
|
+
case .networkError: return "NETWORK_ERROR"
|
|
28
|
+
case .tokenSwapFailed: return "TOKEN_SWAP_FAILED"
|
|
29
|
+
case .tokenSwapParseError: return "TOKEN_SWAP_PARSE_ERROR"
|
|
30
|
+
case .authError: return "AUTH_ERROR"
|
|
22
31
|
case .underlying: return "UNKNOWN"
|
|
23
32
|
}
|
|
24
33
|
}
|
|
@@ -29,6 +38,10 @@ enum SpotifyError: Error {
|
|
|
29
38
|
case .authInProgress: return "Another authentication request is already in progress"
|
|
30
39
|
case .userCancelled: return "Authentication was cancelled by the user"
|
|
31
40
|
case .spotifyNotInstalled: return "The Spotify app is not installed on this device"
|
|
41
|
+
case .networkError(let message, _): return message
|
|
42
|
+
case .tokenSwapFailed(_, let message, _): return message
|
|
43
|
+
case .tokenSwapParseError(let message, _): return message
|
|
44
|
+
case .authError(let message, _): return message
|
|
32
45
|
case .underlying(let message, _): return message
|
|
33
46
|
}
|
|
34
47
|
}
|
|
@@ -38,6 +51,10 @@ enum SpotifyError: Error {
|
|
|
38
51
|
/// full chain rather than collapsing into "undefined reason".
|
|
39
52
|
var underlyingCause: Error? {
|
|
40
53
|
switch self {
|
|
54
|
+
case .networkError(_, let cause): return cause
|
|
55
|
+
case .tokenSwapFailed(_, _, let cause): return cause
|
|
56
|
+
case .tokenSwapParseError(_, let cause): return cause
|
|
57
|
+
case .authError(_, let cause): return cause
|
|
41
58
|
case .underlying(_, let cause): return cause
|
|
42
59
|
default: return nil
|
|
43
60
|
}
|
|
@@ -123,6 +140,10 @@ actor SpotifyAuthCoordinator {
|
|
|
123
140
|
// mutating it here takes effect for the upcoming initiateSession call.
|
|
124
141
|
sptConfiguration.tokenSwapURL = tokenSwapURL
|
|
125
142
|
sptConfiguration.tokenRefreshURL = tokenRefreshURL
|
|
143
|
+
bridge.updateAuthContext(
|
|
144
|
+
tokenSwapURL: tokenSwapURL,
|
|
145
|
+
tokenRefreshURL: tokenRefreshURL
|
|
146
|
+
)
|
|
126
147
|
// alwaysShowAuthorizationDialog is a property on SPTSessionManager, not an
|
|
127
148
|
// SPTAuthorizationOptions flag (the options enum has no such case).
|
|
128
149
|
sessionManager.alwaysShowAuthorizationDialog = showDialog
|
|
@@ -146,6 +167,7 @@ actor SpotifyAuthCoordinator {
|
|
|
146
167
|
func deliver(_ result: Result<SPTSession, Error>) {
|
|
147
168
|
let cont = pending
|
|
148
169
|
pending = nil
|
|
170
|
+
bridge.clearAuthContext()
|
|
149
171
|
cont?.resume(with: result)
|
|
150
172
|
}
|
|
151
173
|
|
|
@@ -168,7 +190,21 @@ actor SpotifyAuthCoordinator {
|
|
|
168
190
|
/// can't satisfy directly. The bridge is a tiny NSObject that hops the call
|
|
169
191
|
/// onto the actor's executor.
|
|
170
192
|
final class SpotifySessionDelegateBridge: NSObject, SPTSessionManagerDelegate {
|
|
193
|
+
private struct AuthContext {
|
|
194
|
+
let tokenSwapURL: URL?
|
|
195
|
+
let tokenRefreshURL: URL?
|
|
196
|
+
}
|
|
197
|
+
|
|
171
198
|
weak var coordinator: SpotifyAuthCoordinator?
|
|
199
|
+
private var authContext: AuthContext?
|
|
200
|
+
|
|
201
|
+
func updateAuthContext(tokenSwapURL: URL?, tokenRefreshURL: URL?) {
|
|
202
|
+
authContext = AuthContext(tokenSwapURL: tokenSwapURL, tokenRefreshURL: tokenRefreshURL)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
func clearAuthContext() {
|
|
206
|
+
authContext = nil
|
|
207
|
+
}
|
|
172
208
|
|
|
173
209
|
func sessionManager(manager _: SPTSessionManager, didInitiate session: SPTSession) {
|
|
174
210
|
NSLog(
|
|
@@ -199,14 +235,194 @@ final class SpotifySessionDelegateBridge: NSObject, SPTSessionManagerDelegate {
|
|
|
199
235
|
/// Translate a few well-known SDK error shapes into our taxonomy.
|
|
200
236
|
private func mapSDKError(_ error: Error) -> Error {
|
|
201
237
|
let nsError = error as NSError
|
|
202
|
-
|
|
203
|
-
|
|
238
|
+
if isUserCancelled(error: nsError) {
|
|
239
|
+
NSLog(
|
|
240
|
+
"[ExpoSpotifySDK] mapSDKError classified=USER_CANCELLED domain=%@ code=%d detail=%@",
|
|
241
|
+
nsError.domain,
|
|
242
|
+
nsError.code,
|
|
243
|
+
describeError(nsError)
|
|
244
|
+
)
|
|
204
245
|
return SpotifyError.userCancelled
|
|
205
246
|
}
|
|
247
|
+
|
|
248
|
+
if let classified = classifyNonCancellationAuthError(nsError) {
|
|
249
|
+
return classified
|
|
250
|
+
}
|
|
251
|
+
|
|
206
252
|
// Keep the original NSError as `cause` so the structured underlying-chain
|
|
207
253
|
// is preserved (Sentry, debug breadcrumbs); the rendered string goes to
|
|
208
254
|
// `message` so JS callers get a single human-readable line.
|
|
209
|
-
|
|
255
|
+
let detail = describeError(nsError)
|
|
256
|
+
NSLog(
|
|
257
|
+
"[ExpoSpotifySDK] mapSDKError classified=UNKNOWN domain=%@ code=%d detail=%@",
|
|
258
|
+
nsError.domain,
|
|
259
|
+
nsError.code,
|
|
260
|
+
detail
|
|
261
|
+
)
|
|
262
|
+
return SpotifyError.underlying(message: detail, cause: nsError)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private func classifyNonCancellationAuthError(_ error: NSError) -> SpotifyError? {
|
|
266
|
+
let detail = describeError(error)
|
|
267
|
+
let lower = detail.lowercased()
|
|
268
|
+
let tokenSwapConfigured = authContext?.tokenSwapURL != nil
|
|
269
|
+
|
|
270
|
+
if isNetworkFailure(error) {
|
|
271
|
+
return SpotifyError.networkError(message: "Network error during Spotify authentication: \(detail)", cause: error)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// OAuth-style rejection from Spotify auth endpoints — evaluated before the
|
|
275
|
+
// tokenSwapConfigured gate so these are never misclassified as token-swap errors.
|
|
276
|
+
if lower.contains("access_denied") || lower.contains("invalid_scope") || lower.contains("invalid_client") ||
|
|
277
|
+
lower.contains("authorization") || lower.contains("oauth") || lower.contains("spotify account")
|
|
278
|
+
{
|
|
279
|
+
return SpotifyError.authError(
|
|
280
|
+
message: "Spotify authorization failed: \(detail)",
|
|
281
|
+
cause: error
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if let status = extractHTTPStatusCode(from: detail), status == 401 || status == 403 {
|
|
286
|
+
return SpotifyError.authError(
|
|
287
|
+
message: "Spotify authorization failed (HTTP \(status)): \(detail)",
|
|
288
|
+
cause: error
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if tokenSwapConfigured || lower.contains("token swap") || lower.contains("token endpoint") || lower.contains("token exchange") {
|
|
293
|
+
if let status = extractHTTPStatusCode(from: detail) {
|
|
294
|
+
return SpotifyError.tokenSwapFailed(
|
|
295
|
+
status: status,
|
|
296
|
+
message: "Token swap server returned HTTP \(status): \(detail)",
|
|
297
|
+
cause: error
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
if lower.contains("parse") || lower.contains("json") || lower.contains("decode") || lower.contains("malformed") {
|
|
301
|
+
return SpotifyError.tokenSwapParseError(
|
|
302
|
+
message: "Token swap response was invalid: \(detail)",
|
|
303
|
+
cause: error
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
return SpotifyError.tokenSwapFailed(
|
|
307
|
+
status: nil,
|
|
308
|
+
message: "Token swap failed: \(detail)",
|
|
309
|
+
cause: error
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return nil
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/// Detect user cancellation from the full NSError chain rather than relying
|
|
317
|
+
/// on a single wrapper domain/code pair.
|
|
318
|
+
private func isUserCancelled(error: NSError) -> Bool {
|
|
319
|
+
var visited = Set<ObjectIdentifier>()
|
|
320
|
+
var stack: [NSError] = [error]
|
|
321
|
+
|
|
322
|
+
while let current = stack.popLast() {
|
|
323
|
+
let id = ObjectIdentifier(current)
|
|
324
|
+
guard visited.insert(id).inserted else { continue }
|
|
325
|
+
|
|
326
|
+
if isKnownCancellationCode(current) || messageLooksCancelled(current) {
|
|
327
|
+
return true
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if let underlying = current.userInfo[NSUnderlyingErrorKey] as? NSError {
|
|
331
|
+
stack.append(underlying)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return false
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private func isKnownCancellationCode(_ error: NSError) -> Bool {
|
|
339
|
+
if error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled {
|
|
340
|
+
return true
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if #available(iOS 12.0, *),
|
|
344
|
+
error.domain == ASWebAuthenticationSessionErrorDomain,
|
|
345
|
+
error.code == ASWebAuthenticationSessionError.canceledLogin.rawValue
|
|
346
|
+
{
|
|
347
|
+
return true
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if error.domain == "SFAuthenticationErrorDomain" && error.code == 1 {
|
|
351
|
+
return true
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return false
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private func isNetworkFailure(_ error: NSError) -> Bool {
|
|
358
|
+
var visited = Set<ObjectIdentifier>()
|
|
359
|
+
var stack: [NSError] = [error]
|
|
360
|
+
|
|
361
|
+
while let current = stack.popLast() {
|
|
362
|
+
let id = ObjectIdentifier(current)
|
|
363
|
+
guard visited.insert(id).inserted else { continue }
|
|
364
|
+
|
|
365
|
+
if current.domain == NSURLErrorDomain,
|
|
366
|
+
current.code != NSURLErrorCancelled
|
|
367
|
+
{
|
|
368
|
+
return true
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if let underlying = current.userInfo[NSUnderlyingErrorKey] as? NSError {
|
|
372
|
+
stack.append(underlying)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return false
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private func messageLooksCancelled(_ error: NSError) -> Bool {
|
|
380
|
+
// Only apply fuzzy text matching in auth/browser domains to avoid mapping
|
|
381
|
+
// unrelated transport or backend failures into USER_CANCELLED.
|
|
382
|
+
guard isLikelyAuthCancellationDomain(error.domain) else {
|
|
383
|
+
return false
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let cancelKeywords = ["cancel", "canceled", "cancelled"]
|
|
387
|
+
let negativeKeywords = [
|
|
388
|
+
"timed out",
|
|
389
|
+
"timeout",
|
|
390
|
+
"network",
|
|
391
|
+
"offline",
|
|
392
|
+
"unreachable",
|
|
393
|
+
"server",
|
|
394
|
+
"unauthorized",
|
|
395
|
+
"forbidden",
|
|
396
|
+
"invalid"
|
|
397
|
+
]
|
|
398
|
+
let messageParts = [
|
|
399
|
+
error.localizedDescription,
|
|
400
|
+
error.localizedFailureReason ?? "",
|
|
401
|
+
error.userInfo[NSLocalizedDescriptionKey] as? String ?? "",
|
|
402
|
+
error.userInfo[NSLocalizedFailureReasonErrorKey] as? String ?? ""
|
|
403
|
+
]
|
|
404
|
+
let combined = messageParts
|
|
405
|
+
.joined(separator: " ")
|
|
406
|
+
.lowercased()
|
|
407
|
+
|
|
408
|
+
if negativeKeywords.contains(where: { combined.contains($0) }) {
|
|
409
|
+
return false
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return cancelKeywords.contains(where: { combined.contains($0) })
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private func isLikelyAuthCancellationDomain(_ domain: String) -> Bool {
|
|
416
|
+
if domain == ASWebAuthenticationSessionErrorDomain || domain == "SFAuthenticationErrorDomain" {
|
|
417
|
+
return true
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// SPT wrappers commonly bubble auth-web errors under their own namespace.
|
|
421
|
+
if domain.lowercased().contains("spotify") && domain.lowercased().contains("auth") {
|
|
422
|
+
return true
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return false
|
|
210
426
|
}
|
|
211
427
|
|
|
212
428
|
/// Build a diagnostic string from an NSError that includes the domain,
|
|
@@ -232,4 +448,29 @@ final class SpotifySessionDelegateBridge: NSObject, SPTSessionManagerDelegate {
|
|
|
232
448
|
}
|
|
233
449
|
return parts.joined(separator: " ")
|
|
234
450
|
}
|
|
451
|
+
|
|
452
|
+
private func extractHTTPStatusCode(from message: String) -> Int? {
|
|
453
|
+
let patterns = [
|
|
454
|
+
#"http\s+([1-5][0-9]{2})"#,
|
|
455
|
+
#"status(?:\s+code)?[:=\s]+([1-5][0-9]{2})"#
|
|
456
|
+
]
|
|
457
|
+
|
|
458
|
+
for pattern in patterns {
|
|
459
|
+
guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else {
|
|
460
|
+
continue
|
|
461
|
+
}
|
|
462
|
+
let range = NSRange(message.startIndex..<message.endIndex, in: message)
|
|
463
|
+
guard let match = regex.firstMatch(in: message, options: [], range: range),
|
|
464
|
+
match.numberOfRanges > 1,
|
|
465
|
+
let codeRange = Range(match.range(at: 1), in: message)
|
|
466
|
+
else {
|
|
467
|
+
continue
|
|
468
|
+
}
|
|
469
|
+
if let code = Int(message[codeRange]) {
|
|
470
|
+
return code
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return nil
|
|
475
|
+
}
|
|
235
476
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wwdrew/expo-spotify-sdk",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.3",
|
|
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",
|