@wwdrew/expo-spotify-sdk 1.0.0 → 2.1.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 +92 -350
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +3 -5
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteCoordinator.kt +6 -64
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteErrorMapping.kt +106 -0
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyTokenSwapClient.kt +2 -7
- package/app.plugin.js +1 -1
- package/build/app-remote/index.d.ts +2 -2
- package/build/app-remote/index.d.ts.map +1 -1
- package/build/app-remote/index.js +11 -24
- package/build/app-remote/index.js.map +1 -1
- package/build/auth/index.d.ts +2 -2
- package/build/auth/index.d.ts.map +1 -1
- package/build/auth/index.js +17 -34
- package/build/auth/index.js.map +1 -1
- package/build/content/index.d.ts.map +1 -1
- package/build/content/index.js +12 -25
- package/build/content/index.js.map +1 -1
- package/build/hooks/index.d.ts +4 -4
- package/build/hooks/index.d.ts.map +1 -1
- package/build/hooks/index.js +38 -115
- package/build/hooks/index.js.map +1 -1
- package/build/images/index.d.ts +2 -2
- package/build/images/index.d.ts.map +1 -1
- package/build/images/index.js +12 -25
- package/build/images/index.js.map +1 -1
- package/build/index.d.ts +0 -18
- package/build/index.d.ts.map +1 -1
- package/build/index.js +0 -27
- package/build/index.js.map +1 -1
- package/build/internal/native-errors.d.ts +14 -0
- package/build/internal/native-errors.d.ts.map +1 -0
- package/build/internal/native-errors.js +29 -0
- package/build/internal/native-errors.js.map +1 -0
- package/build/internal/sync-external-store.d.ts +14 -0
- package/build/internal/sync-external-store.d.ts.map +1 -0
- package/build/internal/sync-external-store.js +36 -0
- package/build/internal/sync-external-store.js.map +1 -0
- package/build/player/index.d.ts +1 -1
- package/build/player/index.d.ts.map +1 -1
- package/build/player/index.js +15 -27
- package/build/player/index.js.map +1 -1
- package/build/uri/index.js.map +1 -1
- package/build/user/index.d.ts +2 -2
- package/build/user/index.d.ts.map +1 -1
- package/build/user/index.js +16 -26
- package/build/user/index.js.map +1 -1
- package/ios/ExpoSpotifySDK.podspec +1 -1
- package/ios/ExpoSpotifySDKModule.swift +1 -1
- package/ios/SpotifyAppRemoteCoordinator.swift +29 -404
- package/ios/SpotifyAppRemoteErrorMapping.swift +110 -0
- package/ios/SpotifyAppRemoteErrors.swift +221 -0
- package/ios/SpotifyAppRemoteMappers.swift +88 -0
- package/package.json +28 -14
- package/plugin/build/index.d.ts +20 -3
- package/plugin/build/index.js +22 -25
- package/plugin/build/types.d.ts +17 -1
- package/plugin/build/withSpotifySdkConfig.d.ts +4 -0
- package/plugin/build/withSpotifySdkConfig.js +27 -0
- package/plugin/index.d.ts +2 -0
- package/plugin/index.js +1 -0
|
@@ -18,7 +18,6 @@ import com.spotify.protocol.types.ListItems
|
|
|
18
18
|
import com.spotify.protocol.types.PlayerState
|
|
19
19
|
import com.spotify.protocol.types.PlaybackSpeed
|
|
20
20
|
import com.spotify.protocol.types.Repeat
|
|
21
|
-
import expo.modules.kotlin.exception.CodedException
|
|
22
21
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
23
22
|
import kotlinx.coroutines.sync.Mutex
|
|
24
23
|
import java.io.File
|
|
@@ -336,7 +335,7 @@ class SpotifyAppRemoteCoordinator {
|
|
|
336
335
|
suspendCancellableCoroutine { continuation ->
|
|
337
336
|
setResultCallback { continuation.resume(Unit) }
|
|
338
337
|
setErrorCallback { throwable ->
|
|
339
|
-
continuation.resumeWithException(
|
|
338
|
+
continuation.resumeWithException(SpotifyAppRemoteErrorMapping.mapPlayerError(throwable, callsite))
|
|
340
339
|
}
|
|
341
340
|
}
|
|
342
341
|
}
|
|
@@ -347,7 +346,7 @@ class SpotifyAppRemoteCoordinator {
|
|
|
347
346
|
return suspendCancellableCoroutine { continuation ->
|
|
348
347
|
setResultCallback { result -> continuation.resume(result) }
|
|
349
348
|
setErrorCallback { throwable ->
|
|
350
|
-
continuation.resumeWithException(
|
|
349
|
+
continuation.resumeWithException(SpotifyAppRemoteErrorMapping.mapPlayerError(throwable, callsite))
|
|
351
350
|
}
|
|
352
351
|
}
|
|
353
352
|
}
|
|
@@ -357,7 +356,7 @@ class SpotifyAppRemoteCoordinator {
|
|
|
357
356
|
return suspendCancellableCoroutine { continuation ->
|
|
358
357
|
setResultCallback { result -> continuation.resume(result) }
|
|
359
358
|
setErrorCallback { throwable ->
|
|
360
|
-
continuation.resumeWithException(
|
|
359
|
+
continuation.resumeWithException(SpotifyAppRemoteErrorMapping.mapUserError(throwable, callsite, uri))
|
|
361
360
|
}
|
|
362
361
|
}
|
|
363
362
|
}
|
|
@@ -366,7 +365,7 @@ class SpotifyAppRemoteCoordinator {
|
|
|
366
365
|
return suspendCancellableCoroutine { continuation ->
|
|
367
366
|
setResultCallback { result -> continuation.resume(result) }
|
|
368
367
|
setErrorCallback { throwable ->
|
|
369
|
-
continuation.resumeWithException(
|
|
368
|
+
continuation.resumeWithException(SpotifyAppRemoteErrorMapping.mapUserError(throwable, callsite, ""))
|
|
370
369
|
}
|
|
371
370
|
}
|
|
372
371
|
}
|
|
@@ -375,7 +374,7 @@ class SpotifyAppRemoteCoordinator {
|
|
|
375
374
|
return suspendCancellableCoroutine { continuation ->
|
|
376
375
|
setResultCallback { result -> continuation.resume(result) }
|
|
377
376
|
setErrorCallback { throwable ->
|
|
378
|
-
continuation.resumeWithException(
|
|
377
|
+
continuation.resumeWithException(SpotifyAppRemoteErrorMapping.mapContentError(throwable, callsite))
|
|
379
378
|
}
|
|
380
379
|
}
|
|
381
380
|
}
|
|
@@ -384,68 +383,11 @@ class SpotifyAppRemoteCoordinator {
|
|
|
384
383
|
return suspendCancellableCoroutine { continuation ->
|
|
385
384
|
setResultCallback { result -> continuation.resume(result) }
|
|
386
385
|
setErrorCallback { throwable ->
|
|
387
|
-
continuation.resumeWithException(
|
|
386
|
+
continuation.resumeWithException(SpotifyAppRemoteErrorMapping.mapImagesError(throwable, callsite))
|
|
388
387
|
}
|
|
389
388
|
}
|
|
390
389
|
}
|
|
391
390
|
|
|
392
|
-
private fun normalizePlayerError(throwable: Throwable, callsite: String): CodedException {
|
|
393
|
-
val msg = throwable.message ?: "Unknown error"
|
|
394
|
-
return when {
|
|
395
|
-
msg.contains("premium", ignoreCase = true) -> PlayerPremiumRequiredException(callsite)
|
|
396
|
-
msg.contains("disconnected", ignoreCase = true) ||
|
|
397
|
-
msg.contains("not connected", ignoreCase = true) ->
|
|
398
|
-
PlayerConnectionLostException("$callsite: $msg", throwable)
|
|
399
|
-
msg.contains("not allowed", ignoreCase = true) ||
|
|
400
|
-
msg.contains("restriction", ignoreCase = true) ->
|
|
401
|
-
PlayerOperationNotAllowedException("$callsite: $msg", throwable)
|
|
402
|
-
else -> PlayerUnknownException("$callsite: $msg", throwable)
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
private fun normalizeUserError(throwable: Throwable, callsite: String, uri: String): CodedException {
|
|
407
|
-
val msg = throwable.message ?: "Unknown error"
|
|
408
|
-
return when {
|
|
409
|
-
msg.contains("disconnected", ignoreCase = true) ||
|
|
410
|
-
msg.contains("not connected", ignoreCase = true) ->
|
|
411
|
-
UserConnectionLostException("$callsite: $msg", throwable)
|
|
412
|
-
msg.contains("uri", ignoreCase = true) ||
|
|
413
|
-
msg.contains("invalid", ignoreCase = true) ->
|
|
414
|
-
UserInvalidURIException(uri, throwable)
|
|
415
|
-
msg.contains("not allowed", ignoreCase = true) ||
|
|
416
|
-
msg.contains("restriction", ignoreCase = true) ->
|
|
417
|
-
UserOperationNotAllowedException("$callsite: $msg", throwable)
|
|
418
|
-
else -> UserUnknownException("$callsite: $msg", throwable)
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
private fun normalizeContentError(throwable: Throwable, callsite: String): CodedException {
|
|
423
|
-
val msg = throwable.message ?: "Unknown error"
|
|
424
|
-
return when {
|
|
425
|
-
msg.contains("disconnected", ignoreCase = true) ||
|
|
426
|
-
msg.contains("not connected", ignoreCase = true) ->
|
|
427
|
-
ContentConnectionLostException("$callsite: $msg", throwable)
|
|
428
|
-
msg.contains("not supported", ignoreCase = true) ||
|
|
429
|
-
msg.contains("unsupported", ignoreCase = true) ->
|
|
430
|
-
ContentApiUnavailableException("$callsite: content API unavailable on this Spotify app version", throwable)
|
|
431
|
-
else -> ContentUnknownException("$callsite: $msg", throwable)
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
private fun normalizeImagesError(throwable: Throwable, callsite: String): CodedException {
|
|
436
|
-
val msg = throwable.message ?: "Unknown error"
|
|
437
|
-
return when {
|
|
438
|
-
msg.contains("disconnected", ignoreCase = true) ||
|
|
439
|
-
msg.contains("not connected", ignoreCase = true) ->
|
|
440
|
-
ImagesNotConnectedException(callsite)
|
|
441
|
-
msg.contains("invalid", ignoreCase = true) ->
|
|
442
|
-
ImagesInvalidURIException("$callsite: invalid image identifier", throwable)
|
|
443
|
-
msg.contains("load", ignoreCase = true) || msg.contains("image", ignoreCase = true) ->
|
|
444
|
-
ImagesLoadFailedException("$callsite: $msg", throwable)
|
|
445
|
-
else -> ImagesUnknownException("$callsite: $msg", throwable)
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
391
|
private fun writeBitmapToTempFile(bitmap: Bitmap, cacheDir: File): File {
|
|
450
392
|
val out = File(cacheDir, "expo-spotify-image-${System.currentTimeMillis()}-${(0..9999).random()}.png")
|
|
451
393
|
FileOutputStream(out).use { stream ->
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
package expo.modules.spotifysdk
|
|
2
|
+
|
|
3
|
+
import com.spotify.android.appremote.api.error.SpotifyConnectionTerminatedException
|
|
4
|
+
import com.spotify.android.appremote.api.error.SpotifyDisconnectedException
|
|
5
|
+
import com.spotify.android.appremote.api.error.SpotifyRemoteServiceException
|
|
6
|
+
import com.spotify.protocol.client.error.RemoteClientException
|
|
7
|
+
import com.spotify.protocol.error.SpotifyAppRemoteException
|
|
8
|
+
import expo.modules.kotlin.exception.CodedException
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Maps Spotify App Remote SDK throwables into per-namespace [CodedException] types.
|
|
12
|
+
*
|
|
13
|
+
* Typed SDK exceptions are checked first; message heuristics are a fallback aligned
|
|
14
|
+
* with [ios/SpotifyAppRemoteErrorMapping.swift]. See [docs/app-remote-error-mapping.md].
|
|
15
|
+
*/
|
|
16
|
+
object SpotifyAppRemoteErrorMapping {
|
|
17
|
+
private fun connectionLostMessage(callsite: String): String =
|
|
18
|
+
"$callsite: connection to Spotify app was terminated"
|
|
19
|
+
|
|
20
|
+
private fun isConnectionTerminated(throwable: Throwable): Boolean =
|
|
21
|
+
throwable is SpotifyConnectionTerminatedException ||
|
|
22
|
+
throwable is SpotifyDisconnectedException
|
|
23
|
+
|
|
24
|
+
private fun messageIndicatesConnectionLost(message: String): Boolean {
|
|
25
|
+
val lower = message.lowercase()
|
|
26
|
+
return lower.contains("disconnected") || lower.contains("not connected")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private fun containsRestriction(message: String): Boolean {
|
|
30
|
+
val lower = message.lowercase()
|
|
31
|
+
return lower.contains("not allowed") || lower.contains("restriction")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Request-level failures from the Spotify protocol layer (mirrors iOS `requestFailedError`). */
|
|
35
|
+
private fun isRequestFailed(throwable: Throwable): Boolean =
|
|
36
|
+
throwable is RemoteClientException ||
|
|
37
|
+
throwable is SpotifyAppRemoteException ||
|
|
38
|
+
throwable is SpotifyRemoteServiceException
|
|
39
|
+
|
|
40
|
+
fun mapPlayerError(throwable: Throwable, callsite: String): CodedException {
|
|
41
|
+
val message = throwable.message ?: "Unknown error"
|
|
42
|
+
if (isConnectionTerminated(throwable) || messageIndicatesConnectionLost(message)) {
|
|
43
|
+
return PlayerConnectionLostException(connectionLostMessage(callsite), throwable)
|
|
44
|
+
}
|
|
45
|
+
if (message.contains("premium", ignoreCase = true)) {
|
|
46
|
+
return PlayerPremiumRequiredException(
|
|
47
|
+
"$callsite: Spotify Premium is required for on-demand playback",
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
if (containsRestriction(message)) {
|
|
51
|
+
return PlayerOperationNotAllowedException("$callsite: $message", throwable)
|
|
52
|
+
}
|
|
53
|
+
if (isRequestFailed(throwable) && message.contains("invalid", ignoreCase = true)) {
|
|
54
|
+
return PlayerInvalidParameterException("$callsite: $message", throwable)
|
|
55
|
+
}
|
|
56
|
+
return PlayerUnknownException("$callsite: $message", throwable)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fun mapUserError(throwable: Throwable, callsite: String, uri: String): CodedException {
|
|
60
|
+
val message = throwable.message ?: "Unknown error"
|
|
61
|
+
if (isConnectionTerminated(throwable) || messageIndicatesConnectionLost(message)) {
|
|
62
|
+
return UserConnectionLostException(connectionLostMessage(callsite), throwable)
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
isRequestFailed(throwable) &&
|
|
66
|
+
(message.contains("uri", ignoreCase = true) || message.contains("invalid", ignoreCase = true))
|
|
67
|
+
) {
|
|
68
|
+
return UserInvalidURIException(uri, throwable)
|
|
69
|
+
}
|
|
70
|
+
if (containsRestriction(message)) {
|
|
71
|
+
return UserOperationNotAllowedException("$callsite: $message", throwable)
|
|
72
|
+
}
|
|
73
|
+
return UserUnknownException("$callsite: $message", throwable)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fun mapContentError(throwable: Throwable, callsite: String): CodedException {
|
|
77
|
+
val message = throwable.message ?: "Unknown error"
|
|
78
|
+
if (isConnectionTerminated(throwable) || messageIndicatesConnectionLost(message)) {
|
|
79
|
+
return ContentConnectionLostException(connectionLostMessage(callsite), throwable)
|
|
80
|
+
}
|
|
81
|
+
if (isRequestFailed(throwable)) {
|
|
82
|
+
val lower = message.lowercase()
|
|
83
|
+
if (lower.contains("not supported") || lower.contains("unsupported")) {
|
|
84
|
+
return ContentApiUnavailableException(
|
|
85
|
+
"$callsite: content API is unavailable on this Spotify app version",
|
|
86
|
+
throwable,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return ContentUnknownException("$callsite: $message", throwable)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fun mapImagesError(throwable: Throwable, callsite: String): CodedException {
|
|
94
|
+
val message = throwable.message ?: "Unknown error"
|
|
95
|
+
if (isConnectionTerminated(throwable) || messageIndicatesConnectionLost(message)) {
|
|
96
|
+
return ImagesNotConnectedException(connectionLostMessage(callsite))
|
|
97
|
+
}
|
|
98
|
+
if (isRequestFailed(throwable) && message.contains("invalid", ignoreCase = true)) {
|
|
99
|
+
return ImagesInvalidURIException("$callsite: invalid image identifier", throwable)
|
|
100
|
+
}
|
|
101
|
+
if (isRequestFailed(throwable)) {
|
|
102
|
+
return ImagesLoadFailedException("$callsite: Spotify rejected image request", throwable)
|
|
103
|
+
}
|
|
104
|
+
return ImagesUnknownException("$callsite: $message", throwable)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -20,16 +20,15 @@ import kotlin.coroutines.resumeWithException
|
|
|
20
20
|
*
|
|
21
21
|
* Server contract:
|
|
22
22
|
* POST {tokenSwapURL}
|
|
23
|
-
* body: code=<auth-code
|
|
23
|
+
* body: code=<auth-code>
|
|
24
24
|
* 200 -> { access_token, expires_in, refresh_token?, scope? }
|
|
25
25
|
*
|
|
26
26
|
* POST {tokenRefreshURL}
|
|
27
|
-
* body: refresh_token=<token
|
|
27
|
+
* body: refresh_token=<token>
|
|
28
28
|
* 200 -> { access_token, expires_in, refresh_token?, scope? }
|
|
29
29
|
*/
|
|
30
30
|
class SpotifyTokenSwapClient(
|
|
31
31
|
private val sdkVersion: String,
|
|
32
|
-
private val clientId: String,
|
|
33
32
|
) {
|
|
34
33
|
private val client: OkHttpClient = OkHttpClient.Builder()
|
|
35
34
|
.connectTimeout(30, TimeUnit.SECONDS)
|
|
@@ -41,14 +40,11 @@ class SpotifyTokenSwapClient(
|
|
|
41
40
|
|
|
42
41
|
suspend fun swap(
|
|
43
42
|
code: String,
|
|
44
|
-
redirectUri: String,
|
|
45
43
|
tokenSwapURL: String,
|
|
46
44
|
requestedScopes: List<String>,
|
|
47
45
|
): SpotifySessionPayload {
|
|
48
46
|
val body = FormBody.Builder()
|
|
49
47
|
.add("code", code)
|
|
50
|
-
.add("redirect_uri", redirectUri)
|
|
51
|
-
.add("client_id", clientId)
|
|
52
48
|
.build()
|
|
53
49
|
val request = Request.Builder()
|
|
54
50
|
.url(tokenSwapURL)
|
|
@@ -66,7 +62,6 @@ class SpotifyTokenSwapClient(
|
|
|
66
62
|
): SpotifySessionPayload {
|
|
67
63
|
val body = FormBody.Builder()
|
|
68
64
|
.add("refresh_token", refreshToken)
|
|
69
|
-
.add("client_id", clientId)
|
|
70
65
|
.build()
|
|
71
66
|
val request = Request.Builder()
|
|
72
67
|
.url(tokenRefreshURL)
|
package/app.plugin.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
module.exports = require("./plugin/build");
|
|
1
|
+
module.exports = require("./plugin/build/withSpotifySdkConfig");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EventSubscription } from "expo-modules-core";
|
|
2
|
-
import { AppRemoteErrorCode } from "./error";
|
|
1
|
+
import type { EventSubscription } from "expo-modules-core";
|
|
2
|
+
import { type AppRemoteErrorCode } from "./error";
|
|
3
3
|
export type { AppRemoteErrorCode } from "./error";
|
|
4
4
|
export { AppRemoteError } from "./error";
|
|
5
5
|
/** Current state of the IPC connection to the Spotify app. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/app-remote/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/app-remote/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG3D,OAAO,EAAkB,KAAK,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAGlE,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAMzC,8DAA8D;AAC9D,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,CAAC;AAE1E,oDAAoD;AACpD,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,eAAe,CAAC;CACxB;AAED,8CAA8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,kBAAkB,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;CACjB;AAqBD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,SAAS;IACpB;;;;;;;;;;;OAWG;oCACkB,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;IAM3C;;;OAGG;+BACW,OAAO,CAAC,IAAI,CAAC;IAI3B;;;;OAIG;gCACY,OAAO;IAItB;;;OAGG;uCACmB,OAAO,CAAC,eAAe,CAAC;IAI9C;;;;;;;;;OASG;2BACS,CAAC,SAAS,uBAAuB,GAAG,iBAAiB,SACxD,CAAC,YACE,CAAC,SAAS,uBAAuB,GACvC,CAAC,KAAK,EAAE,0BAA0B,KAAK,IAAI,GAC3C,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,KACxC,iBAAiB;CAUZ,CAAC"}
|
|
@@ -1,33 +1,20 @@
|
|
|
1
1
|
import ExpoSpotifySDKModule from "../ExpoSpotifySDKModule";
|
|
2
2
|
import { AppRemoteError } from "./error";
|
|
3
|
+
import { createNativeErrorRethrow } from "../internal/native-errors";
|
|
3
4
|
export { AppRemoteError } from "./error";
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// Internal helpers
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
function rethrowAsAppRemoteError(err) {
|
|
19
|
-
if (err instanceof AppRemoteError)
|
|
20
|
-
throw err;
|
|
21
|
-
if (err instanceof Error) {
|
|
22
|
-
const reason = unwrapReason(err.message);
|
|
23
|
-
const maybeCode = err.code;
|
|
24
|
-
if (maybeCode && VALID_APPREMOTE_CODES.has(maybeCode)) {
|
|
25
|
-
throw new AppRemoteError(maybeCode, reason);
|
|
26
|
-
}
|
|
27
|
-
throw new AppRemoteError("UNKNOWN", reason);
|
|
28
|
-
}
|
|
29
|
-
throw new AppRemoteError("UNKNOWN", String(err));
|
|
30
|
-
}
|
|
8
|
+
const rethrowAsAppRemoteError = createNativeErrorRethrow({
|
|
9
|
+
ErrorClass: AppRemoteError,
|
|
10
|
+
unknownCode: "UNKNOWN",
|
|
11
|
+
validCodes: new Set([
|
|
12
|
+
"CONNECTION_FAILED",
|
|
13
|
+
"CONNECTION_LOST",
|
|
14
|
+
"NOT_CONNECTED",
|
|
15
|
+
"UNKNOWN",
|
|
16
|
+
]),
|
|
17
|
+
});
|
|
31
18
|
// ---------------------------------------------------------------------------
|
|
32
19
|
// AppRemote namespace
|
|
33
20
|
// ---------------------------------------------------------------------------
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/app-remote/index.ts"],"names":[],"mappings":"AAEA,OAAO,oBAAoB,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/app-remote/index.ts"],"names":[],"mappings":"AAEA,OAAO,oBAAoB,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAA2B,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAGrE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAoBzC,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,MAAM,uBAAuB,GAAG,wBAAwB,CAAC;IACvD,UAAU,EAAE,cAAc;IAC1B,WAAW,EAAE,SAAS;IACtB,UAAU,EAAE,IAAI,GAAG,CAAqB;QACtC,mBAAmB;QACnB,iBAAiB;QACjB,eAAe;QACf,SAAS;KACV,CAAC;CACH,CAAC,CAAC;AAEH,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,WAAmB;QACzB,OAAO,oBAAoB,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,KAAK,CAC7D,uBAAuB,CACxB,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,UAAU;QACR,OAAO,oBAAoB,CAAC,mBAAmB,EAAE,CAAC;IACpD,CAAC;IAED;;;;OAIG;IACH,WAAW;QACT,OAAO,oBAAoB,CAAC,oBAAoB,EAAE,CAAC;IACrD,CAAC;IAED;;;OAGG;IACH,kBAAkB;QAChB,OAAO,oBAAoB,CAAC,2BAA2B,EAAE,CAAC;IAC5D,CAAC;IAED;;;;;;;;;OASG;IACH,WAAW,CACT,KAAQ,EACR,QAEyC;QAEzC,MAAM,WAAW,GACf,KAAK,KAAK,uBAAuB;YAC/B,CAAC,CAAC,yBAAyB;YAC3B,CAAC,CAAC,mBAAmB,CAAC;QAC1B,OAAO,oBAAoB,CAAC,WAAW,CACrC,WAAW,EACX,QAAoC,CAChB,CAAC;IACzB,CAAC;CACO,CAAC","sourcesContent":["import type { EventSubscription } from \"expo-modules-core\";\n\nimport ExpoSpotifySDKModule from \"../ExpoSpotifySDKModule\";\nimport { AppRemoteError, type AppRemoteErrorCode } from \"./error\";\nimport { createNativeErrorRethrow } from \"../internal/native-errors\";\n\nexport type { AppRemoteErrorCode } from \"./error\";\nexport { AppRemoteError } from \"./error\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Current state of the IPC connection to the Spotify app. */\nexport type ConnectionState = \"disconnected\" | \"connecting\" | \"connected\";\n\n/** Payload of the `connectionStateChange` event. */\nexport interface ConnectionStateChangeEvent {\n state: ConnectionState;\n}\n\n/** Payload of the `connectionError` event. */\nexport interface ConnectionErrorEvent {\n code: AppRemoteErrorCode;\n message: string;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst rethrowAsAppRemoteError = createNativeErrorRethrow({\n ErrorClass: AppRemoteError,\n unknownCode: \"UNKNOWN\",\n validCodes: new Set<AppRemoteErrorCode>([\n \"CONNECTION_FAILED\",\n \"CONNECTION_LOST\",\n \"NOT_CONNECTED\",\n \"UNKNOWN\",\n ]),\n});\n\n// ---------------------------------------------------------------------------\n// AppRemote namespace\n// ---------------------------------------------------------------------------\n\n/**\n * Spotify App Remote namespace. Manages the IPC connection to the running\n * Spotify app. All `Player`, `User`, `Content`, and `Images` calls require an\n * active connection established via `AppRemote.connect()`.\n *\n * @example\n * ```ts\n * import { AppRemote } from \"@wwdrew/expo-spotify-sdk\";\n *\n * // Connect using the access token from Auth.authenticate()\n * await AppRemote.connect(session.accessToken);\n *\n * const sub = AppRemote.addListener(\"connectionStateChange\", ({ state }) => {\n * console.log(\"connection state:\", state);\n * });\n *\n * // later:\n * await AppRemote.disconnect();\n * sub.remove();\n * ```\n */\nexport const AppRemote = {\n /**\n * Opens a connection to the running Spotify app using the provided access\n * token. Resolves when the connection is established; rejects with an\n * {@link AppRemoteError} if the connection fails.\n *\n * **Android note:** The access token is accepted for API parity with iOS but\n * the Android App Remote SDK does not accept it directly — it uses the\n * session cached in the Spotify app from your earlier `Auth.authenticate()`\n * call. Ensure `Auth.authenticate()` has succeeded before calling `connect()`.\n *\n * Calling `connect()` while already connected is a no-op.\n */\n connect(accessToken: string): Promise<void> {\n return ExpoSpotifySDKModule.appRemoteConnect(accessToken).catch(\n rethrowAsAppRemoteError,\n );\n },\n\n /**\n * Disconnects from the Spotify app. Safe to call when already disconnected.\n * Resolves once the disconnection is complete.\n */\n disconnect(): Promise<void> {\n return ExpoSpotifySDKModule.appRemoteDisconnect();\n },\n\n /**\n * Returns `true` if currently connected to the Spotify app.\n * This is a synchronous snapshot — subscribe to `\"connectionStateChange\"`\n * for reactive updates.\n */\n isConnected(): boolean {\n return ExpoSpotifySDKModule.appRemoteIsConnected();\n },\n\n /**\n * Returns the current {@link ConnectionState} synchronously.\n * Subscribe to `\"connectionStateChange\"` for reactive updates.\n */\n getConnectionState(): Promise<ConnectionState> {\n return ExpoSpotifySDKModule.appRemoteGetConnectionState();\n },\n\n /**\n * Subscribes to connection lifecycle events.\n *\n * | Event | Payload | When |\n * |---|---|---|\n * | `\"connectionStateChange\"` | `{ state: ConnectionState }` | State transitions |\n * | `\"connectionError\"` | `{ code, message }` | Connection failures and drops |\n *\n * Returns a `Subscription` — call `.remove()` to unsubscribe.\n */\n addListener<E extends \"connectionStateChange\" | \"connectionError\">(\n event: E,\n listener: E extends \"connectionStateChange\"\n ? (event: ConnectionStateChangeEvent) => void\n : (event: ConnectionErrorEvent) => void,\n ): EventSubscription {\n const nativeEvent =\n event === \"connectionStateChange\"\n ? \"onConnectionStateChange\"\n : \"onConnectionError\";\n return ExpoSpotifySDKModule.addListener(\n nativeEvent,\n listener as (event: unknown) => void,\n ) as EventSubscription;\n },\n} as const;\n"]}
|
package/build/auth/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EventSubscription } from "expo-modules-core";
|
|
2
|
-
import { AuthErrorCode } from "./error";
|
|
1
|
+
import type { EventSubscription } from "expo-modules-core";
|
|
2
|
+
import { type AuthErrorCode } from "./error";
|
|
3
3
|
export type { AuthErrorCode } from "./error";
|
|
4
4
|
export { AuthError } from "./error";
|
|
5
5
|
export interface SpotifySession {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,OAAO,EAAa,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAGxD,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAMpC,MAAM,WAAW,cAAc;IAC7B,0BAA0B;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,uDAAuD;IACvD,cAAc,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,MAAM,EAAE,YAAY,EAAE,CAAC;CACxB;AAED,MAAM,MAAM,YAAY,GACpB,kBAAkB,GAClB,0BAA0B,GAC1B,4BAA4B,GAC5B,6BAA6B,GAC7B,oBAAoB,GACpB,WAAW,GACX,uBAAuB,GACvB,6BAA6B,GAC7B,yBAAyB,GACzB,wBAAwB,GACxB,oBAAoB,GACpB,kBAAkB,GAClB,eAAe,GACf,2BAA2B,GAC3B,qBAAqB,GACrB,mBAAmB,GACnB,iBAAiB,GACjB,mBAAmB,CAAC;AAExB,MAAM,WAAW,kBAAkB;IACjC,gEAAgE;IAChE,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,eAAe,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AA+DzE;;;;;;;;;GASG;AACH,eAAO,MAAM,IAAI;IACf;;OAEG;gCACY,OAAO;IAItB;;;OAGG;oCACkB,kBAAkB,KAAG,OAAO,CAAC,cAAc,CAAC;IAsBjE;;;;OAIG;+BACa,aAAa,KAAG,OAAO,CAAC,cAAc,CAAC;IAgBvD;;;;;;;OAOG;kCACc,OAAO,CAAC,IAAI,CAAC;IAO9B;;;;;;;;;;;;;;;;;;;OAmBG;kCAEM,eAAe,YACZ,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,KAC5C,iBAAiB;CAMZ,CAAC"}
|
package/build/auth/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Platform } from "expo-modules-core";
|
|
2
2
|
import ExpoSpotifySDKModule from "../ExpoSpotifySDKModule";
|
|
3
3
|
import { AuthError } from "./error";
|
|
4
|
+
import { createNativeErrorRethrow } from "../internal/native-errors";
|
|
4
5
|
export { AuthError } from "./error";
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Internal helpers
|
|
@@ -10,40 +11,22 @@ const ANDROID_TOKEN_FLOW_WARNING = "[expo-spotify-sdk] You are using Auth.authen
|
|
|
10
11
|
"the actual granted scopes through this path; see the README's " +
|
|
11
12
|
"'Android implicit (TOKEN) flow is not recommended' section.";
|
|
12
13
|
let warnedAboutAndroidTokenFlow = false;
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
function rethrowAsAuthError(err) {
|
|
31
|
-
if (err instanceof AuthError)
|
|
32
|
-
throw err;
|
|
33
|
-
if (err instanceof Error) {
|
|
34
|
-
const reason = unwrapReason(err.message);
|
|
35
|
-
const maybeCode = err.code;
|
|
36
|
-
if (maybeCode && VALID_AUTH_CODES.has(maybeCode)) {
|
|
37
|
-
throw new AuthError(maybeCode, reason);
|
|
38
|
-
}
|
|
39
|
-
const m = reason.match(LEGACY_CODE_PREFIX_RE);
|
|
40
|
-
if (m && VALID_AUTH_CODES.has(m[1])) {
|
|
41
|
-
throw new AuthError(m[1], m[2]);
|
|
42
|
-
}
|
|
43
|
-
throw new AuthError("UNKNOWN", reason);
|
|
44
|
-
}
|
|
45
|
-
throw new AuthError("UNKNOWN", String(err));
|
|
46
|
-
}
|
|
14
|
+
const rethrowAsAuthError = createNativeErrorRethrow({
|
|
15
|
+
ErrorClass: AuthError,
|
|
16
|
+
unknownCode: "UNKNOWN",
|
|
17
|
+
legacyCodePrefix: true,
|
|
18
|
+
validCodes: new Set([
|
|
19
|
+
"USER_CANCELLED",
|
|
20
|
+
"AUTH_IN_PROGRESS",
|
|
21
|
+
"INVALID_CONFIG",
|
|
22
|
+
"NETWORK_ERROR",
|
|
23
|
+
"TOKEN_SWAP_FAILED",
|
|
24
|
+
"TOKEN_SWAP_PARSE_ERROR",
|
|
25
|
+
"SPOTIFY_NOT_INSTALLED",
|
|
26
|
+
"AUTH_ERROR",
|
|
27
|
+
"UNKNOWN",
|
|
28
|
+
]),
|
|
29
|
+
});
|
|
47
30
|
function normaliseSession(raw) {
|
|
48
31
|
if (!raw || typeof raw !== "object") {
|
|
49
32
|
throw new AuthError("UNKNOWN", "Native module returned a non-object session");
|
package/build/auth/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventSubscription, Platform } from \"expo-modules-core\";\n\nimport ExpoSpotifySDKModule from \"../ExpoSpotifySDKModule\";\nimport { AuthError, AuthErrorCode } from \"./error\";\n\nexport type { AuthErrorCode } from \"./error\";\nexport { AuthError } from \"./error\";\n\n// ---------------------------------------------------------------------------\n// Auth-specific types\n// ---------------------------------------------------------------------------\n\nexport interface SpotifySession {\n /** OAuth access token. */\n accessToken: string;\n /**\n * OAuth refresh token. `null` on Android when no `tokenSwapURL` is provided\n * (the Spotify Android SDK does not expose a refresh token for implicit\n * grants).\n */\n refreshToken: string | null;\n /** Expiration timestamp as Unix epoch milliseconds. */\n expirationDate: number;\n /** Scopes the access token was granted (or requested, on Android implicit). */\n scopes: SpotifyScope[];\n}\n\nexport type SpotifyScope =\n | \"ugc-image-upload\"\n | \"user-read-playback-state\"\n | \"user-modify-playback-state\"\n | \"user-read-currently-playing\"\n | \"app-remote-control\"\n | \"streaming\"\n | \"playlist-read-private\"\n | \"playlist-read-collaborative\"\n | \"playlist-modify-private\"\n | \"playlist-modify-public\"\n | \"user-follow-modify\"\n | \"user-follow-read\"\n | \"user-top-read\"\n | \"user-read-recently-played\"\n | \"user-library-modify\"\n | \"user-library-read\"\n | \"user-read-email\"\n | \"user-read-private\";\n\nexport interface AuthenticateConfig {\n /** OAuth scopes to request. Must contain at least one entry. */\n scopes: SpotifyScope[];\n /**\n * If supplied, requests an authorization code rather than an implicit\n * token, then POSTs the code to this URL to exchange it for tokens.\n * **Required on Android** to receive a usable `refreshToken`.\n */\n tokenSwapURL?: string;\n /**\n * Used by the iOS SDK to refresh access tokens automatically, and by\n * `Auth.refresh()` on both platforms.\n */\n tokenRefreshURL?: string;\n /**\n * If `true`, forces Spotify to show the authorization dialog even when the\n * user already has an active session. Defaults to `false`.\n */\n showDialog?: boolean;\n}\n\nexport interface RefreshConfig {\n /** The refresh token from a previous `Auth.authenticate()` call. */\n refreshToken: string;\n /** URL of your token refresh server endpoint. */\n tokenRefreshURL: string;\n /**\n * Scopes that were granted by the previous session. Used as a fallback when\n * the refresh response omits the `scope` field.\n */\n scopes?: SpotifyScope[];\n}\n\n/**\n * Payload delivered to `Auth.addListener(\"sessionChange\", ...)` subscribers.\n */\nexport type SessionChangeEvent =\n | { type: \"didInitiate\"; session: SpotifySession }\n | { type: \"didRenew\"; session: SpotifySession }\n | { type: \"didFail\"; error: { code: AuthErrorCode; message: string } };\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst ANDROID_TOKEN_FLOW_WARNING =\n \"[expo-spotify-sdk] You are using Auth.authenticate on Android without a \" +\n \"tokenSwapURL. The Spotify Android SDK does NOT return a refresh token or \" +\n \"the actual granted scopes through this path; see the README's \" +\n \"'Android implicit (TOKEN) flow is not recommended' section.\";\n\nlet warnedAboutAndroidTokenFlow = false;\n\nconst VALID_AUTH_CODES = new Set<AuthErrorCode>([\n \"USER_CANCELLED\",\n \"AUTH_IN_PROGRESS\",\n \"INVALID_CONFIG\",\n \"NETWORK_ERROR\",\n \"TOKEN_SWAP_FAILED\",\n \"TOKEN_SWAP_PARSE_ERROR\",\n \"SPOTIFY_NOT_INSTALLED\",\n \"AUTH_ERROR\",\n \"UNKNOWN\",\n]);\n\nconst CAUSE_SEPARATOR = \"→ Caused by: \";\nconst LEGACY_CODE_PREFIX_RE = /^([A-Z_][A-Z0-9_]*):\\s*(.*)$/s;\n\nfunction unwrapReason(message: string): string {\n const idx = message.lastIndexOf(CAUSE_SEPARATOR);\n return idx === -1 ? message : message.slice(idx + CAUSE_SEPARATOR.length);\n}\n\nfunction rethrowAsAuthError(err: unknown): never {\n if (err instanceof AuthError) throw err;\n if (err instanceof Error) {\n const reason = unwrapReason(err.message);\n const maybeCode = (err as Error & { code?: string }).code;\n if (maybeCode && VALID_AUTH_CODES.has(maybeCode as AuthErrorCode)) {\n throw new AuthError(maybeCode as AuthErrorCode, reason);\n }\n const m = reason.match(LEGACY_CODE_PREFIX_RE);\n if (m && VALID_AUTH_CODES.has(m[1] as AuthErrorCode)) {\n throw new AuthError(m[1] as AuthErrorCode, m[2]);\n }\n throw new AuthError(\"UNKNOWN\", reason);\n }\n throw new AuthError(\"UNKNOWN\", String(err));\n}\n\nfunction normaliseSession(raw: unknown): SpotifySession {\n if (!raw || typeof raw !== \"object\") {\n throw new AuthError(\"UNKNOWN\", \"Native module returned a non-object session\");\n }\n const r = raw as Record<string, unknown>;\n const accessToken = r.accessToken;\n if (typeof accessToken !== \"string\" || accessToken.length === 0) {\n throw new AuthError(\"UNKNOWN\", \"Session is missing accessToken\");\n }\n const expirationDate = r.expirationDate;\n if (typeof expirationDate !== \"number\") {\n throw new AuthError(\"UNKNOWN\", \"Session is missing expirationDate\");\n }\n const refreshTokenRaw = r.refreshToken;\n const refreshToken =\n typeof refreshTokenRaw === \"string\" && refreshTokenRaw.length > 0\n ? refreshTokenRaw\n : null;\n const scopesRaw = r.scopes;\n const scopes = Array.isArray(scopesRaw)\n ? (scopesRaw.filter((s) => typeof s === \"string\") as SpotifyScope[])\n : [];\n return { accessToken, refreshToken, expirationDate, scopes };\n}\n\n// ---------------------------------------------------------------------------\n// Auth namespace\n// ---------------------------------------------------------------------------\n\n/**\n * Spotify Auth namespace. Handles OAuth authentication and session lifecycle.\n *\n * @example\n * ```ts\n * import { Auth } from \"@wwdrew/expo-spotify-sdk\";\n *\n * const session = await Auth.authenticate({ scopes: [\"streaming\"] });\n * ```\n */\nexport const Auth = {\n /**\n * Returns `true` if the Spotify app is installed on the device.\n */\n isAvailable(): boolean {\n return ExpoSpotifySDKModule.isAvailable();\n },\n\n /**\n * Starts a Spotify OAuth flow. Resolves with a {@link SpotifySession};\n * rejects with an {@link AuthError} carrying a `code`.\n */\n authenticate(config: AuthenticateConfig): Promise<SpotifySession> {\n if (!config.scopes || config.scopes.length === 0) {\n return Promise.reject(\n new AuthError(\"INVALID_CONFIG\", \"scopes must contain at least one entry\"),\n );\n }\n if (\n Platform.OS === \"android\" &&\n !config.tokenSwapURL &&\n !warnedAboutAndroidTokenFlow\n ) {\n warnedAboutAndroidTokenFlow = true;\n console.warn(ANDROID_TOKEN_FLOW_WARNING);\n }\n return ExpoSpotifySDKModule.authenticateAsync(config)\n .then(normaliseSession)\n .catch(rethrowAsAuthError);\n },\n\n /**\n * Exchanges a refresh token for a new access token via your token refresh\n * server. Resolves with a fresh {@link SpotifySession}; rejects with an\n * {@link AuthError}.\n */\n refresh(config: RefreshConfig): Promise<SpotifySession> {\n if (!config.refreshToken) {\n return Promise.reject(\n new AuthError(\"INVALID_CONFIG\", \"refreshToken is required\"),\n );\n }\n if (!config.tokenRefreshURL) {\n return Promise.reject(\n new AuthError(\"INVALID_CONFIG\", \"tokenRefreshURL is required\"),\n );\n }\n return ExpoSpotifySDKModule.refreshSessionAsync(config)\n .then(normaliseSession)\n .catch(rethrowAsAuthError);\n },\n\n /**\n * Forcibly cancel any in-flight `Auth.authenticate()` call. No-op on\n * Android (the Android coordinator self-cleans via structured concurrency).\n *\n * Use before `Auth.authenticate()` to defensively clear any leaked iOS\n * coordinator state (the `SPTSessionManager` delegate callbacks are not\n * guaranteed to fire).\n */\n cancelPending(): Promise<void> {\n if (Platform.OS !== \"ios\") {\n return Promise.resolve();\n }\n return ExpoSpotifySDKModule.cancelPendingAuthAsync();\n },\n\n /**\n * Subscribes to session lifecycle events.\n *\n * Events fire for every `Auth.authenticate()` and `Auth.refresh()` call,\n * regardless of whether the call was awaited. Useful for persisting tokens\n * in a central store without coupling the store to the call sites.\n *\n * Returns a `Subscription` — call `.remove()` to unsubscribe.\n *\n * @example\n * ```ts\n * const sub = Auth.addListener(\"sessionChange\", (event) => {\n * if (event.type === \"didInitiate\" || event.type === \"didRenew\") {\n * store.setSession(event.session);\n * }\n * });\n * // later:\n * sub.remove();\n * ```\n */\n addListener(\n event: \"sessionChange\",\n listener: (event: SessionChangeEvent) => void,\n ): EventSubscription {\n return ExpoSpotifySDKModule.addListener(\n \"onSessionChange\",\n listener,\n ) as EventSubscription;\n },\n} as const;\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { EventSubscription } from \"expo-modules-core\";\nimport { Platform } from \"expo-modules-core\";\n\nimport ExpoSpotifySDKModule from \"../ExpoSpotifySDKModule\";\nimport { AuthError, type AuthErrorCode } from \"./error\";\nimport { createNativeErrorRethrow } from \"../internal/native-errors\";\n\nexport type { AuthErrorCode } from \"./error\";\nexport { AuthError } from \"./error\";\n\n// ---------------------------------------------------------------------------\n// Auth-specific types\n// ---------------------------------------------------------------------------\n\nexport interface SpotifySession {\n /** OAuth access token. */\n accessToken: string;\n /**\n * OAuth refresh token. `null` on Android when no `tokenSwapURL` is provided\n * (the Spotify Android SDK does not expose a refresh token for implicit\n * grants).\n */\n refreshToken: string | null;\n /** Expiration timestamp as Unix epoch milliseconds. */\n expirationDate: number;\n /** Scopes the access token was granted (or requested, on Android implicit). */\n scopes: SpotifyScope[];\n}\n\nexport type SpotifyScope =\n | \"ugc-image-upload\"\n | \"user-read-playback-state\"\n | \"user-modify-playback-state\"\n | \"user-read-currently-playing\"\n | \"app-remote-control\"\n | \"streaming\"\n | \"playlist-read-private\"\n | \"playlist-read-collaborative\"\n | \"playlist-modify-private\"\n | \"playlist-modify-public\"\n | \"user-follow-modify\"\n | \"user-follow-read\"\n | \"user-top-read\"\n | \"user-read-recently-played\"\n | \"user-library-modify\"\n | \"user-library-read\"\n | \"user-read-email\"\n | \"user-read-private\";\n\nexport interface AuthenticateConfig {\n /** OAuth scopes to request. Must contain at least one entry. */\n scopes: SpotifyScope[];\n /**\n * If supplied, requests an authorization code rather than an implicit\n * token, then POSTs the code to this URL to exchange it for tokens.\n * **Required on Android** to receive a usable `refreshToken`.\n */\n tokenSwapURL?: string;\n /**\n * Used by the iOS SDK to refresh access tokens automatically, and by\n * `Auth.refresh()` on both platforms.\n */\n tokenRefreshURL?: string;\n /**\n * If `true`, forces Spotify to show the authorization dialog even when the\n * user already has an active session. Defaults to `false`.\n */\n showDialog?: boolean;\n}\n\nexport interface RefreshConfig {\n /** The refresh token from a previous `Auth.authenticate()` call. */\n refreshToken: string;\n /** URL of your token refresh server endpoint. */\n tokenRefreshURL: string;\n /**\n * Scopes that were granted by the previous session. Used as a fallback when\n * the refresh response omits the `scope` field.\n */\n scopes?: SpotifyScope[];\n}\n\n/**\n * Payload delivered to `Auth.addListener(\"sessionChange\", ...)` subscribers.\n */\nexport type SessionChangeEvent =\n | { type: \"didInitiate\"; session: SpotifySession }\n | { type: \"didRenew\"; session: SpotifySession }\n | { type: \"didFail\"; error: { code: AuthErrorCode; message: string } };\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst ANDROID_TOKEN_FLOW_WARNING =\n \"[expo-spotify-sdk] You are using Auth.authenticate on Android without a \" +\n \"tokenSwapURL. The Spotify Android SDK does NOT return a refresh token or \" +\n \"the actual granted scopes through this path; see the README's \" +\n \"'Android implicit (TOKEN) flow is not recommended' section.\";\n\nlet warnedAboutAndroidTokenFlow = false;\n\nconst rethrowAsAuthError = createNativeErrorRethrow({\n ErrorClass: AuthError,\n unknownCode: \"UNKNOWN\",\n legacyCodePrefix: true,\n validCodes: new Set<AuthErrorCode>([\n \"USER_CANCELLED\",\n \"AUTH_IN_PROGRESS\",\n \"INVALID_CONFIG\",\n \"NETWORK_ERROR\",\n \"TOKEN_SWAP_FAILED\",\n \"TOKEN_SWAP_PARSE_ERROR\",\n \"SPOTIFY_NOT_INSTALLED\",\n \"AUTH_ERROR\",\n \"UNKNOWN\",\n ]),\n});\n\nfunction normaliseSession(raw: unknown): SpotifySession {\n if (!raw || typeof raw !== \"object\") {\n throw new AuthError(\n \"UNKNOWN\",\n \"Native module returned a non-object session\",\n );\n }\n const r = raw as Record<string, unknown>;\n const accessToken = r.accessToken;\n if (typeof accessToken !== \"string\" || accessToken.length === 0) {\n throw new AuthError(\"UNKNOWN\", \"Session is missing accessToken\");\n }\n const expirationDate = r.expirationDate;\n if (typeof expirationDate !== \"number\") {\n throw new AuthError(\"UNKNOWN\", \"Session is missing expirationDate\");\n }\n const refreshTokenRaw = r.refreshToken;\n const refreshToken =\n typeof refreshTokenRaw === \"string\" && refreshTokenRaw.length > 0\n ? refreshTokenRaw\n : null;\n const scopesRaw = r.scopes;\n const scopes = Array.isArray(scopesRaw)\n ? (scopesRaw.filter((s) => typeof s === \"string\") as SpotifyScope[])\n : [];\n return { accessToken, refreshToken, expirationDate, scopes };\n}\n\n// ---------------------------------------------------------------------------\n// Auth namespace\n// ---------------------------------------------------------------------------\n\n/**\n * Spotify Auth namespace. Handles OAuth authentication and session lifecycle.\n *\n * @example\n * ```ts\n * import { Auth } from \"@wwdrew/expo-spotify-sdk\";\n *\n * const session = await Auth.authenticate({ scopes: [\"streaming\"] });\n * ```\n */\nexport const Auth = {\n /**\n * Returns `true` if the Spotify app is installed on the device.\n */\n isAvailable(): boolean {\n return ExpoSpotifySDKModule.isAvailable();\n },\n\n /**\n * Starts a Spotify OAuth flow. Resolves with a {@link SpotifySession};\n * rejects with an {@link AuthError} carrying a `code`.\n */\n authenticate(config: AuthenticateConfig): Promise<SpotifySession> {\n if (!config.scopes || config.scopes.length === 0) {\n return Promise.reject(\n new AuthError(\n \"INVALID_CONFIG\",\n \"scopes must contain at least one entry\",\n ),\n );\n }\n if (\n Platform.OS === \"android\" &&\n !config.tokenSwapURL &&\n !warnedAboutAndroidTokenFlow\n ) {\n warnedAboutAndroidTokenFlow = true;\n console.warn(ANDROID_TOKEN_FLOW_WARNING);\n }\n return ExpoSpotifySDKModule.authenticateAsync(config)\n .then(normaliseSession)\n .catch(rethrowAsAuthError);\n },\n\n /**\n * Exchanges a refresh token for a new access token via your token refresh\n * server. Resolves with a fresh {@link SpotifySession}; rejects with an\n * {@link AuthError}.\n */\n refresh(config: RefreshConfig): Promise<SpotifySession> {\n if (!config.refreshToken) {\n return Promise.reject(\n new AuthError(\"INVALID_CONFIG\", \"refreshToken is required\"),\n );\n }\n if (!config.tokenRefreshURL) {\n return Promise.reject(\n new AuthError(\"INVALID_CONFIG\", \"tokenRefreshURL is required\"),\n );\n }\n return ExpoSpotifySDKModule.refreshSessionAsync(config)\n .then(normaliseSession)\n .catch(rethrowAsAuthError);\n },\n\n /**\n * Forcibly cancel any in-flight `Auth.authenticate()` call. No-op on\n * Android (the Android coordinator self-cleans via structured concurrency).\n *\n * Use before `Auth.authenticate()` to defensively clear any leaked iOS\n * coordinator state (the `SPTSessionManager` delegate callbacks are not\n * guaranteed to fire).\n */\n cancelPending(): Promise<void> {\n if (Platform.OS !== \"ios\") {\n return Promise.resolve();\n }\n return ExpoSpotifySDKModule.cancelPendingAuthAsync();\n },\n\n /**\n * Subscribes to session lifecycle events.\n *\n * Events fire for every `Auth.authenticate()` and `Auth.refresh()` call,\n * regardless of whether the call was awaited. Useful for persisting tokens\n * in a central store without coupling the store to the call sites.\n *\n * Returns a `Subscription` — call `.remove()` to unsubscribe.\n *\n * @example\n * ```ts\n * const sub = Auth.addListener(\"sessionChange\", (event) => {\n * if (event.type === \"didInitiate\" || event.type === \"didRenew\") {\n * store.setSession(event.session);\n * }\n * });\n * // later:\n * sub.remove();\n * ```\n */\n addListener(\n event: \"sessionChange\",\n listener: (event: SessionChangeEvent) => void,\n ): EventSubscription {\n return ExpoSpotifySDKModule.addListener(\n \"onSessionChange\",\n listener,\n ) as EventSubscription;\n },\n} as const;\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/content/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/content/index.ts"],"names":[],"mappings":"AAQA,YAAY,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE1E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;CAC1B;AAaD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,OAAO;IAClB;;OAEG;gDAC8B,WAAW,KAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAMrE;;OAEG;iCACe,WAAW,KAAG,OAAO,CAAC,WAAW,EAAE,CAAC;CAK9C,CAAC"}
|
package/build/content/index.js
CHANGED
|
@@ -1,33 +1,20 @@
|
|
|
1
|
-
export { ContentError } from "./error";
|
|
2
1
|
// ---------------------------------------------------------------------------
|
|
3
2
|
// Types
|
|
4
3
|
// ---------------------------------------------------------------------------
|
|
5
4
|
import ExpoSpotifySDKModule from "../ExpoSpotifySDKModule";
|
|
6
5
|
import { ContentError } from "./error";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"UNKNOWN",
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (err instanceof ContentError)
|
|
20
|
-
throw err;
|
|
21
|
-
if (err instanceof Error) {
|
|
22
|
-
const reason = unwrapReason(err.message);
|
|
23
|
-
const maybeCode = err.code;
|
|
24
|
-
if (maybeCode && VALID_CONTENT_CODES.has(maybeCode)) {
|
|
25
|
-
throw new ContentError(maybeCode, reason);
|
|
26
|
-
}
|
|
27
|
-
throw new ContentError("UNKNOWN", reason);
|
|
28
|
-
}
|
|
29
|
-
throw new ContentError("UNKNOWN", String(err));
|
|
30
|
-
}
|
|
6
|
+
import { createNativeErrorRethrow } from "../internal/native-errors";
|
|
7
|
+
export { ContentError } from "./error";
|
|
8
|
+
const rethrowAsContentError = createNativeErrorRethrow({
|
|
9
|
+
ErrorClass: ContentError,
|
|
10
|
+
unknownCode: "UNKNOWN",
|
|
11
|
+
validCodes: new Set([
|
|
12
|
+
"NOT_CONNECTED",
|
|
13
|
+
"CONNECTION_LOST",
|
|
14
|
+
"CONTENT_API_UNAVAILABLE",
|
|
15
|
+
"UNKNOWN",
|
|
16
|
+
]),
|
|
17
|
+
});
|
|
31
18
|
/**
|
|
32
19
|
* Spotify Content namespace. Browse Spotify's curated content tree.
|
|
33
20
|
* Requires `AppRemote.connect()` to be resolved before any call.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/content/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/content/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,QAAQ;AACR,8EAA8E;AAE9E,OAAO,oBAAoB,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAyB,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAGrE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAkBvC,MAAM,qBAAqB,GAAG,wBAAwB,CAAC;IACrD,UAAU,EAAE,YAAY;IACxB,WAAW,EAAE,SAAS;IACtB,UAAU,EAAE,IAAI,GAAG,CAAmB;QACpC,eAAe;QACf,iBAAiB;QACjB,yBAAyB;QACzB,SAAS;KACV,CAAC;CACH,CAAC,CAAC;AAEH;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB;;OAEG;IACH,0BAA0B,CAAC,IAAiB;QAC1C,OAAO,oBAAoB,CAAC,iCAAiC,CAAC,IAAI,CAAC,CAAC,KAAK,CACvE,qBAAqB,CACtB,CAAC;IACJ,CAAC;IAED;;OAEG;IACH,WAAW,CAAC,IAAiB;QAC3B,OAAO,oBAAoB,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,KAAK,CACxD,qBAAqB,CACtB,CAAC;IACJ,CAAC;CACO,CAAC","sourcesContent":["// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nimport ExpoSpotifySDKModule from \"../ExpoSpotifySDKModule\";\nimport { ContentError, type ContentErrorCode } from \"./error\";\nimport { createNativeErrorRethrow } from \"../internal/native-errors\";\n\nexport type { ContentErrorCode } from \"./error\";\nexport { ContentError } from \"./error\";\n\nexport type ContentType = \"default\" | \"navigation\" | \"fitness\" | \"gaming\";\n\nexport interface ContentItem {\n title: string | null;\n subtitle: string | null;\n contentDescription: string | null;\n identifier: string;\n uri: string;\n imageIdentifier: string | null;\n isAvailableOffline: boolean;\n isPlayable: boolean;\n isContainer: boolean;\n isPinned: boolean;\n children?: ContentItem[];\n}\n\nconst rethrowAsContentError = createNativeErrorRethrow({\n ErrorClass: ContentError,\n unknownCode: \"UNKNOWN\",\n validCodes: new Set<ContentErrorCode>([\n \"NOT_CONNECTED\",\n \"CONNECTION_LOST\",\n \"CONTENT_API_UNAVAILABLE\",\n \"UNKNOWN\",\n ]),\n});\n\n/**\n * Spotify Content namespace. Browse Spotify's curated content tree.\n * Requires `AppRemote.connect()` to be resolved before any call.\n *\n * @example\n * ```ts\n * import { Content } from \"@wwdrew/expo-spotify-sdk\";\n *\n * const items = await Content.getRecommendedContentItems(\"default\");\n * ```\n */\nexport const Content = {\n /**\n * Returns Spotify-curated recommended content for the given feed type.\n */\n getRecommendedContentItems(type: ContentType): Promise<ContentItem[]> {\n return ExpoSpotifySDKModule.contentGetRecommendedContentItems(type).catch(\n rethrowAsContentError,\n );\n },\n\n /**\n * Returns children of a browsable (container) content item.\n */\n getChildren(item: ContentItem): Promise<ContentItem[]> {\n return ExpoSpotifySDKModule.contentGetChildren(item).catch(\n rethrowAsContentError,\n );\n },\n} as const;\n"]}
|
package/build/hooks/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ConnectionState } from "../app-remote";
|
|
2
|
-
import { SpotifySession } from "../auth";
|
|
3
|
-
import { PlayerState, Track } from "../player";
|
|
4
|
-
import { Capabilities, LibraryState } from "../user";
|
|
1
|
+
import { type ConnectionState } from "../app-remote";
|
|
2
|
+
import { type SpotifySession } from "../auth";
|
|
3
|
+
import { type PlayerState, type Track } from "../player";
|
|
5
4
|
import type { SpotifyURI as SpotifyURIType } from "../uri";
|
|
5
|
+
import { type Capabilities, type LibraryState } from "../user";
|
|
6
6
|
/**
|
|
7
7
|
* Returns the current Spotify OAuth session, or `null` when not authenticated.
|
|
8
8
|
* Updates automatically whenever `Auth.authenticate()` or `Auth.refresh()`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,eAAe,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAEpD,OAAO,EAAU,KAAK,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAQ,MAAM,SAAS,CAAC;AAsMrE;;;;;;GAMG;AACH,wBAAgB,UAAU,IAAI,cAAc,GAAG,IAAI,CAMlD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,IAAI,eAAe,CAMpD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,cAAc,IAAI,WAAW,GAAG,IAAI,CAMnD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,KAAK,GAAG,IAAI,CAE9C;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAGtC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,YAAY,GAAG,IAAI,CAMrD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,YAAY,GAAG,IAAI,CAMxE"}
|