@wwdrew/expo-spotify-sdk 0.8.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +404 -140
- package/android/build.gradle +12 -2
- package/android/consumer-rules.pro +5 -0
- package/android/libs/SETUP.md +29 -0
- package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +146 -9
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteCoordinator.kt +556 -0
- package/android/src/main/java/expo/modules/spotifysdk/SpotifyErrors.kt +112 -0
- package/app.plugin.js +1 -1
- package/build/ExpoSpotifySDKModule.web.d.ts +23 -0
- package/build/ExpoSpotifySDKModule.web.d.ts.map +1 -1
- package/build/ExpoSpotifySDKModule.web.js +74 -5
- package/build/ExpoSpotifySDKModule.web.js.map +1 -1
- package/build/app-remote/error.d.ts +8 -0
- package/build/app-remote/error.d.ts.map +1 -0
- package/build/app-remote/error.js +10 -0
- package/build/app-remote/error.js.map +1 -0
- package/build/app-remote/index.d.ts +79 -0
- package/build/app-remote/index.d.ts.map +1 -0
- package/build/app-remote/index.js +110 -0
- package/build/app-remote/index.js.map +1 -0
- package/build/auth/error.d.ts +8 -0
- package/build/auth/error.d.ts.map +1 -0
- package/build/auth/error.js +10 -0
- package/build/auth/error.js.map +1 -0
- package/build/auth/index.d.ts +124 -0
- package/build/auth/index.d.ts.map +1 -0
- package/build/auth/index.js +162 -0
- package/build/auth/index.js.map +1 -0
- package/build/content/error.d.ts +8 -0
- package/build/content/error.d.ts.map +1 -0
- package/build/content/error.js +10 -0
- package/build/content/error.js.map +1 -0
- package/build/content/index.d.ts +38 -0
- package/build/content/index.d.ts.map +1 -0
- package/build/content/index.js +56 -0
- package/build/content/index.js.map +1 -0
- package/build/error.d.ts +24 -0
- package/build/error.d.ts.map +1 -0
- package/build/error.js +25 -0
- package/build/error.js.map +1 -0
- package/build/hooks/index.d.ts +90 -0
- package/build/hooks/index.d.ts.map +1 -0
- package/build/hooks/index.js +326 -0
- package/build/hooks/index.js.map +1 -0
- package/build/images/error.d.ts +8 -0
- package/build/images/error.d.ts.map +1 -0
- package/build/images/error.js +10 -0
- package/build/images/error.js.map +1 -0
- package/build/images/index.d.ts +36 -0
- package/build/images/index.d.ts.map +1 -0
- package/build/images/index.js +59 -0
- package/build/images/index.js.map +1 -0
- package/build/index.d.ts +44 -56
- package/build/index.d.ts.map +1 -1
- package/build/index.js +49 -141
- package/build/index.js.map +1 -1
- package/build/player/error.d.ts +8 -0
- package/build/player/error.d.ts.map +1 -0
- package/build/player/error.js +10 -0
- package/build/player/error.js.map +1 -0
- package/build/player/index.d.ts +140 -0
- package/build/player/index.d.ts.map +1 -0
- package/build/player/index.js +137 -0
- package/build/player/index.js.map +1 -0
- package/build/uri/index.d.ts +55 -0
- package/build/uri/index.d.ts.map +1 -0
- package/build/uri/index.js +57 -0
- package/build/uri/index.js.map +1 -0
- package/build/user/error.d.ts +8 -0
- package/build/user/error.d.ts.map +1 -0
- package/build/user/error.js +10 -0
- package/build/user/error.js.map +1 -0
- package/build/user/index.d.ts +44 -0
- package/build/user/index.d.ts.map +1 -0
- package/build/user/index.js +97 -0
- package/build/user/index.js.map +1 -0
- package/ios/ExpoSpotifySDK.podspec +1 -1
- package/ios/ExpoSpotifySDKModule.swift +227 -5
- package/ios/SpotifyAppRemoteCoordinator.swift +939 -0
- package/ios/SpotifyAuthCoordinator.swift +53 -13
- package/ios/SpotifyTokenRefreshClient.swift +28 -0
- package/package.json +29 -16
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
1
2
|
import Foundation
|
|
2
3
|
import SpotifyiOS
|
|
3
4
|
|
|
@@ -9,7 +10,7 @@ enum SpotifyError: Error {
|
|
|
9
10
|
case authInProgress
|
|
10
11
|
case userCancelled
|
|
11
12
|
case spotifyNotInstalled
|
|
12
|
-
case underlying(Error)
|
|
13
|
+
case underlying(message: String, cause: Error)
|
|
13
14
|
|
|
14
15
|
var code: String {
|
|
15
16
|
switch self {
|
|
@@ -23,22 +24,54 @@ enum SpotifyError: Error {
|
|
|
23
24
|
|
|
24
25
|
var message: String {
|
|
25
26
|
switch self {
|
|
26
|
-
case .invalidConfiguration(let m):
|
|
27
|
-
case .authInProgress:
|
|
28
|
-
case .userCancelled:
|
|
29
|
-
case .spotifyNotInstalled:
|
|
30
|
-
case .underlying(let
|
|
27
|
+
case .invalidConfiguration(let m): return m
|
|
28
|
+
case .authInProgress: return "Another authentication request is already in progress"
|
|
29
|
+
case .userCancelled: return "Authentication was cancelled by the user"
|
|
30
|
+
case .spotifyNotInstalled: return "The Spotify app is not installed on this device"
|
|
31
|
+
case .underlying(let message, _): return message
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// The original error that caused this failure, if any. Surfaced as the
|
|
36
|
+
/// JS-facing exception's `cause` so debugging / Sentry breadcrumbs keep the
|
|
37
|
+
/// full chain rather than collapsing into "undefined reason".
|
|
38
|
+
var underlyingCause: Error? {
|
|
39
|
+
switch self {
|
|
40
|
+
case .underlying(_, let cause): return cause
|
|
41
|
+
default: return nil
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
44
|
}
|
|
34
45
|
|
|
46
|
+
/// `Exception` subclass that projects a `SpotifyError`'s `code` and `message`
|
|
47
|
+
/// through `expo-modules-core`'s exception bridge so JS receives the structured
|
|
48
|
+
/// code and a meaningful reason — not the default "undefined reason" placeholder.
|
|
49
|
+
///
|
|
50
|
+
/// Without this wrapper an `AsyncFunction` rejection collapses to
|
|
51
|
+
/// `FunctionCallException` → `cause.reason = "undefined reason"` because the
|
|
52
|
+
/// previously-used `GenericException<String>` does not override `reason`.
|
|
53
|
+
final class SpotifyAuthException: Exception, @unchecked Sendable {
|
|
54
|
+
private let spotifyCode: String
|
|
55
|
+
private let spotifyMessage: String
|
|
56
|
+
|
|
57
|
+
init(_ error: SpotifyError, file: String = #fileID, line: UInt = #line, function: String = #function) {
|
|
58
|
+
self.spotifyCode = error.code
|
|
59
|
+
self.spotifyMessage = error.message
|
|
60
|
+
super.init(file: file, line: line, function: function)
|
|
61
|
+
self.cause = error.underlyingCause
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override var code: String { spotifyCode }
|
|
65
|
+
override var reason: String { spotifyMessage }
|
|
66
|
+
}
|
|
67
|
+
|
|
35
68
|
/// `actor` ensures `pending` is mutated only on the actor's serial executor;
|
|
36
69
|
/// no manual locks needed.
|
|
37
70
|
actor SpotifyAuthCoordinator {
|
|
38
71
|
static let shared: SpotifyAuthCoordinator? = SpotifyAuthCoordinator.create()
|
|
39
72
|
|
|
40
73
|
private let sptConfiguration: SPTConfiguration
|
|
41
|
-
private let sessionManager: SPTSessionManager
|
|
74
|
+
nonisolated(unsafe) private let sessionManager: SPTSessionManager
|
|
42
75
|
private let bridge: SpotifySessionDelegateBridge
|
|
43
76
|
private var pending: CheckedContinuation<SPTSession, Error>?
|
|
44
77
|
|
|
@@ -72,7 +105,15 @@ actor SpotifyAuthCoordinator {
|
|
|
72
105
|
/// Synchronous URL-handler dispatcher used by the AppDelegate / SceneDelegate
|
|
73
106
|
/// subscribers to forward redirects to the SPTSessionManager.
|
|
74
107
|
nonisolated func handleOpenURL(_ url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
|
|
75
|
-
|
|
108
|
+
let openSession: () -> Bool = { [self] in
|
|
109
|
+
MainActor.assumeIsolated {
|
|
110
|
+
sessionManager.application(UIApplication.shared, open: url, options: options)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if Thread.isMainThread {
|
|
114
|
+
return openSession()
|
|
115
|
+
}
|
|
116
|
+
return DispatchQueue.main.sync(execute: openSession)
|
|
76
117
|
}
|
|
77
118
|
|
|
78
119
|
func authenticate(
|
|
@@ -167,11 +208,10 @@ final class SpotifySessionDelegateBridge: NSObject, SPTSessionManagerDelegate {
|
|
|
167
208
|
if description.contains("cancel") {
|
|
168
209
|
return SpotifyError.userCancelled
|
|
169
210
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
))
|
|
211
|
+
// Keep the original NSError as `cause` so the structured underlying-chain
|
|
212
|
+
// is preserved (Sentry, debug breadcrumbs); the rendered string goes to
|
|
213
|
+
// `message` so JS callers get a single human-readable line.
|
|
214
|
+
return SpotifyError.underlying(message: describeError(nsError), cause: nsError)
|
|
175
215
|
}
|
|
176
216
|
|
|
177
217
|
/// Build a diagnostic string from an NSError that includes the domain,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
1
2
|
import Foundation
|
|
2
3
|
|
|
3
4
|
enum SpotifyRefreshError: Error {
|
|
@@ -28,6 +29,33 @@ enum SpotifyRefreshError: Error {
|
|
|
28
29
|
return m
|
|
29
30
|
}
|
|
30
31
|
}
|
|
32
|
+
|
|
33
|
+
/// The original error that caused this failure, if any. Surfaced as the
|
|
34
|
+
/// JS-facing exception's `cause`.
|
|
35
|
+
var underlyingCause: Error? {
|
|
36
|
+
switch self {
|
|
37
|
+
case .network(let err): return err
|
|
38
|
+
default: return nil
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// `Exception` subclass that projects a `SpotifyRefreshError`'s `code` and
|
|
44
|
+
/// `message` through `expo-modules-core` so JS sees the structured taxonomy
|
|
45
|
+
/// instead of "undefined reason". Mirrors `SpotifyAuthException`.
|
|
46
|
+
final class SpotifyRefreshException: Exception, @unchecked Sendable {
|
|
47
|
+
private let spotifyCode: String
|
|
48
|
+
private let spotifyMessage: String
|
|
49
|
+
|
|
50
|
+
init(_ error: SpotifyRefreshError, file: String = #fileID, line: UInt = #line, function: String = #function) {
|
|
51
|
+
self.spotifyCode = error.code
|
|
52
|
+
self.spotifyMessage = error.message
|
|
53
|
+
super.init(file: file, line: line, function: function)
|
|
54
|
+
self.cause = error.underlyingCause
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override var code: String { spotifyCode }
|
|
58
|
+
override var reason: String { spotifyMessage }
|
|
31
59
|
}
|
|
32
60
|
|
|
33
61
|
struct SpotifyRefreshResult {
|
package/package.json
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wwdrew/expo-spotify-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Expo module wrapping the native Spotify iOS (v5) and Android (v4) SDKs for OAuth authentication",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Expo module wrapping the native Spotify iOS (v5) and Android (v4) SDKs for OAuth authentication and App Remote playback control",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./build/index.d.ts",
|
|
10
|
+
"default": "./build/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./plugin": {
|
|
13
|
+
"types": "./plugin/build/index.d.ts",
|
|
14
|
+
"default": "./plugin/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
7
17
|
"scripts": {
|
|
8
|
-
"build": "
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"prepublishOnly": "
|
|
14
|
-
"typecheck": "
|
|
15
|
-
"expo-module": "expo-module",
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"build:plugin": "tsc -p plugin/tsconfig.json",
|
|
20
|
+
"clean": "rm -rf build plugin/build",
|
|
21
|
+
"lint": "eslint src",
|
|
22
|
+
"test": "jest --rootDir plugin --config plugin/jest.config.js",
|
|
23
|
+
"prepublishOnly": "yarn clean && yarn build && yarn build:plugin",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
16
25
|
"open:ios": "open -a \"Xcode\" example/ios",
|
|
17
26
|
"open:android": "open -a \"Android Studio\" example/android"
|
|
18
27
|
},
|
|
@@ -39,17 +48,21 @@
|
|
|
39
48
|
"license": "MIT",
|
|
40
49
|
"homepage": "https://github.com/wwdrew/expo-spotify-sdk#readme",
|
|
41
50
|
"devDependencies": {
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
51
|
+
"@babel/runtime": "^7.29.7",
|
|
52
|
+
"@expo/config-plugins": "^56.0.8",
|
|
53
|
+
"@types/react": "^19.0.0",
|
|
54
|
+
"babel-preset-expo": "~56.0.0",
|
|
44
55
|
"eslint": "^9.0.0",
|
|
45
|
-
"expo-module-scripts": "^
|
|
46
|
-
"expo-modules-core": "^
|
|
56
|
+
"expo-module-scripts": "^56.0.2",
|
|
57
|
+
"expo-modules-core": "^56.0.13",
|
|
47
58
|
"jest": "^29.7.0",
|
|
48
|
-
"prettier": "^3.0.0"
|
|
59
|
+
"prettier": "^3.0.0",
|
|
60
|
+
"typescript": "^5.9.0"
|
|
49
61
|
},
|
|
50
62
|
"peerDependencies": {
|
|
51
63
|
"expo": "*",
|
|
52
64
|
"react": "*",
|
|
53
65
|
"react-native": "*"
|
|
54
|
-
}
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {}
|
|
55
68
|
}
|
package/plugin/build/index.d.ts
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
import { ConfigPlugin } from "@expo/config-plugins";
|
|
2
1
|
import { SpotifyConfig } from "./types";
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Typed config plugin helper for `app.config.ts` (Expo SDK 56+).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* import withSpotifySdk from "@wwdrew/expo-spotify-sdk/plugin";
|
|
7
|
+
*
|
|
8
|
+
* export default ({ config }) => ({
|
|
9
|
+
* ...config,
|
|
10
|
+
* plugins: [
|
|
11
|
+
* withSpotifySdk({
|
|
12
|
+
* clientID: "your-spotify-client-id",
|
|
13
|
+
* scheme: "myapp",
|
|
14
|
+
* host: "spotify-auth",
|
|
15
|
+
* }),
|
|
16
|
+
* ],
|
|
17
|
+
* });
|
|
18
|
+
*/
|
|
19
|
+
declare const _default: (props: SpotifyConfig) => [string, SpotifyConfig];
|
|
20
|
+
export default _default;
|
|
21
|
+
export type { SpotifyConfig, SpotifyScopes } from "./types";
|
package/plugin/build/index.js
CHANGED
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
};
|
|
26
|
-
exports.withSpotifySdkConfig = withSpotifySdkConfig;
|
|
27
|
-
exports.default = exports.withSpotifySdkConfig;
|
|
3
|
+
const PACKAGE_NAME = "@wwdrew/expo-spotify-sdk";
|
|
4
|
+
/**
|
|
5
|
+
* Typed config plugin helper for `app.config.ts` (Expo SDK 56+).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import withSpotifySdk from "@wwdrew/expo-spotify-sdk/plugin";
|
|
9
|
+
*
|
|
10
|
+
* export default ({ config }) => ({
|
|
11
|
+
* ...config,
|
|
12
|
+
* plugins: [
|
|
13
|
+
* withSpotifySdk({
|
|
14
|
+
* clientID: "your-spotify-client-id",
|
|
15
|
+
* scheme: "myapp",
|
|
16
|
+
* host: "spotify-auth",
|
|
17
|
+
* }),
|
|
18
|
+
* ],
|
|
19
|
+
* });
|
|
20
|
+
*/
|
|
21
|
+
exports.default = (props) => [
|
|
22
|
+
PACKAGE_NAME,
|
|
23
|
+
props,
|
|
24
|
+
];
|
package/plugin/build/types.d.ts
CHANGED
|
@@ -8,7 +8,23 @@ export type SpotifyScopes = "ugc-image-upload" | "user-read-playback-state" | "u
|
|
|
8
8
|
* Configuration accepted by the `@wwdrew/expo-spotify-sdk` Expo config plugin.
|
|
9
9
|
*
|
|
10
10
|
* @example
|
|
11
|
-
* // app.config.ts
|
|
11
|
+
* // app.config.ts (Expo SDK 56+ — typed import)
|
|
12
|
+
* import withSpotifySdk from "@wwdrew/expo-spotify-sdk/plugin";
|
|
13
|
+
*
|
|
14
|
+
* export default ({ config }) => ({
|
|
15
|
+
* ...config,
|
|
16
|
+
* plugins: [
|
|
17
|
+
* withSpotifySdk({
|
|
18
|
+
* clientID: "<spotify-client-id>",
|
|
19
|
+
* scheme: "myapp",
|
|
20
|
+
* host: "spotify-auth",
|
|
21
|
+
* redirectPathPattern: ".*",
|
|
22
|
+
* }),
|
|
23
|
+
* ],
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* // app.config.ts / app.json (string tuple — all SDK versions)
|
|
12
28
|
* export default {
|
|
13
29
|
* plugins: [
|
|
14
30
|
* ["@wwdrew/expo-spotify-sdk", {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withSpotifySdkConfig = void 0;
|
|
4
|
+
const withSpotifyAndroidAppBuildGradle_1 = require("./android/withSpotifyAndroidAppBuildGradle");
|
|
5
|
+
const withSpotifyConfigValues_1 = require("./ios/withSpotifyConfigValues");
|
|
6
|
+
const withSpotifyQueryScheme_1 = require("./ios/withSpotifyQueryScheme");
|
|
7
|
+
const withSpotifyURLScheme_1 = require("./ios/withSpotifyURLScheme");
|
|
8
|
+
const withSpotifySdkConfig = (config, spotifyConfig) => {
|
|
9
|
+
if (!spotifyConfig.host) {
|
|
10
|
+
throw new Error("Missing required Spotify config value: host");
|
|
11
|
+
}
|
|
12
|
+
if (!spotifyConfig.scheme) {
|
|
13
|
+
throw new Error("Missing required Spotify config value: scheme");
|
|
14
|
+
}
|
|
15
|
+
if (!spotifyConfig.clientID) {
|
|
16
|
+
throw new Error("Missing required Spotify config value: clientID");
|
|
17
|
+
}
|
|
18
|
+
// Android specific
|
|
19
|
+
config = (0, withSpotifyAndroidAppBuildGradle_1.withSpotifyAndroidAppBuildGradle)(config, spotifyConfig);
|
|
20
|
+
// iOS specific
|
|
21
|
+
config = (0, withSpotifyConfigValues_1.withSpotifyConfigValues)(config, spotifyConfig);
|
|
22
|
+
config = (0, withSpotifyQueryScheme_1.withSpotifyQueryScheme)(config, spotifyConfig);
|
|
23
|
+
config = (0, withSpotifyURLScheme_1.withSpotifyURLScheme)(config, spotifyConfig);
|
|
24
|
+
return config;
|
|
25
|
+
};
|
|
26
|
+
exports.withSpotifySdkConfig = withSpotifySdkConfig;
|
|
27
|
+
exports.default = exports.withSpotifySdkConfig;
|