@wwdrew/expo-spotify-sdk 0.5.0 → 0.7.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.
Files changed (60) hide show
  1. package/LICENSE +21 -3
  2. package/README.md +346 -96
  3. package/android/build.gradle +28 -22
  4. package/android/consumer-rules.pro +8 -0
  5. package/android/src/main/AndroidManifest.xml +3 -0
  6. package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +133 -147
  7. package/android/src/main/java/expo/modules/spotifysdk/SpotifyAuthCoordinator.kt +36 -0
  8. package/android/src/main/java/expo/modules/spotifysdk/SpotifyAuthorizationContract.kt +46 -0
  9. package/android/src/main/java/expo/modules/spotifysdk/SpotifyConfig.kt +84 -0
  10. package/android/src/main/java/expo/modules/spotifysdk/SpotifyErrors.kt +34 -0
  11. package/android/src/main/java/expo/modules/spotifysdk/SpotifyTokenSwapClient.kt +154 -0
  12. package/build/ExpoSpotifySDK.types.d.ts +84 -1
  13. package/build/ExpoSpotifySDK.types.d.ts.map +1 -1
  14. package/build/ExpoSpotifySDK.types.js +12 -1
  15. package/build/ExpoSpotifySDK.types.js.map +1 -1
  16. package/build/ExpoSpotifySDKModule.web.d.ts +7 -3
  17. package/build/ExpoSpotifySDKModule.web.d.ts.map +1 -1
  18. package/build/ExpoSpotifySDKModule.web.js +17 -7
  19. package/build/ExpoSpotifySDKModule.web.js.map +1 -1
  20. package/build/index.d.ts +39 -2
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js +117 -4
  23. package/build/index.js.map +1 -1
  24. package/expo-module.config.json +1 -1
  25. package/ios/ExpoSpotifyAppDelegate.swift +14 -0
  26. package/ios/ExpoSpotifyConfiguration.swift +33 -15
  27. package/ios/ExpoSpotifySDK.podspec +2 -4
  28. package/ios/ExpoSpotifySDKModule.swift +98 -26
  29. package/ios/SPTScopeSerializer.swift +40 -39
  30. package/ios/SpotifyAuthCoordinator.swift +186 -0
  31. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTConfiguration.h +1 -1
  32. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTError.h +6 -7
  33. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTScope.h +43 -39
  34. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTSessionManager.h +8 -4
  35. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SpotifyiOS.h +1 -1
  36. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Info.plist +0 -0
  37. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/SpotifyiOS +0 -0
  38. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTConfiguration.h +1 -1
  39. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTError.h +6 -7
  40. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTScope.h +43 -39
  41. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTSessionManager.h +8 -4
  42. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SpotifyiOS.h +1 -1
  43. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Info.plist +0 -0
  44. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/SpotifyiOS +0 -0
  45. package/ios/SpotifyTokenRefreshClient.swift +118 -0
  46. package/package.json +12 -11
  47. package/plugin/build/android/withSpotifyAndroidAppBuildGradle.js +15 -10
  48. package/plugin/build/ios/withSpotifyConfigValues.js +12 -7
  49. package/plugin/build/ios/withSpotifyURLScheme.d.ts +1 -1
  50. package/plugin/build/ios/withSpotifyURLScheme.js +9 -8
  51. package/plugin/build/types.d.ts +35 -0
  52. package/plugin/build/types.js +0 -19
  53. package/ios/ExpoSpotifyAuthDelegate.swift +0 -13
  54. package/ios/ExpoSpotifySessionManager+SessionManagerDelegate.swift +0 -16
  55. package/ios/ExpoSpotifySessionManager.swift +0 -90
  56. package/ios/SpotifySDK/SpotifyiOS.xcframework/_CodeSignature/CodeDirectory +0 -0
  57. package/ios/SpotifySDK/SpotifyiOS.xcframework/_CodeSignature/CodeRequirements +0 -0
  58. package/ios/SpotifySDK/SpotifyiOS.xcframework/_CodeSignature/CodeRequirements-1 +0 -0
  59. package/ios/SpotifySDK/SpotifyiOS.xcframework/_CodeSignature/CodeResources +0 -1118
  60. package/ios/SpotifySDK/SpotifyiOS.xcframework/_CodeSignature/CodeSignature +0 -0
@@ -30,42 +30,46 @@
30
30
  typedef NS_OPTIONS(NSUInteger, SPTScope) {
31
31
  /// Read access to user's private playlists.
32
32
  SPTPlaylistReadPrivateScope NS_SWIFT_NAME(playlistReadPrivate) = (1 << 0),
33
- /// Include collaborative playlists when requesting a user's playlists.
34
- SPTPlaylistReadCollaborativeScope NS_SWIFT_NAME(playlistReadCollaborative) = (1 << 1),
35
- /// Write access to a user's public playlists.
36
- SPTPlaylistModifyPublicScope NS_SWIFT_NAME(playlistModifyPublic) = (1 << 2),
37
- /// Write access to a user's private playlists.
38
- SPTPlaylistModifyPrivateScope NS_SWIFT_NAME(playlistModifyPrivate) = (1 << 3),
39
- /// Read access to the list of artists and other users that the user follows.
40
- SPTUserFollowReadScope NS_SWIFT_NAME(userFollowRead) = (1 << 4),
41
- /// Write/delete access to the list of artists and other users that the user follows.
42
- SPTUserFollowModifyScope NS_SWIFT_NAME(userFollowModify) = (1 << 5),
43
- /// Read access to a user's "Your Music" library.
44
- SPTUserLibraryReadScope NS_SWIFT_NAME(userLibraryRead) = (1 << 6),
45
- /// Write/delete access to a user's "Your Music" library.
46
- SPTUserLibraryModifyScope NS_SWIFT_NAME(userLibraryModify) = (1 << 7),
47
- /// Read access to the user's birthdate.
48
- SPTUserReadBirthDateScope NS_SWIFT_NAME(userReadBirthDate) = (1 << 8),
49
- /// Read access to user’s email address.
50
- SPTUserReadEmailScope NS_SWIFT_NAME(userReadEmail) = (1 << 9),
51
- /// Read access to user’s subscription details (type of user account).
52
- SPTUserReadPrivateScope NS_SWIFT_NAME(userReadPrivate) = (1 << 10),
53
- /// Read access to a user's top artists and tracks.
54
- SPTUserTopReadScope NS_SWIFT_NAME(userTopRead) = (1 << 11),
55
- /// Upload user generated content images
56
- SPTUGCImageUploadScope NS_SWIFT_NAME(ugcImageUpload) = (1 << 12),
57
- /// Control playback of a Spotify track.
58
- SPTStreamingScope NS_SWIFT_NAME(streaming) = (1 << 13),
59
- /// Use App Remote to control playback in the Spotify app
60
- SPTAppRemoteControlScope NS_SWIFT_NAME(appRemoteControl) = (1 << 14),
61
- /// Read access to a user’s player state.
62
- SPTUserReadPlaybackStateScope NS_SWIFT_NAME(userReadPlaybackState) = (1 << 15),
63
- /// Write access to a user’s playback state
64
- SPTUserModifyPlaybackStateScope NS_SWIFT_NAME(userModifyPlaybackState) = (1 << 16),
65
- /// Read access to a user’s currently playing track
66
- SPTUserReadCurrentlyPlayingScope NS_SWIFT_NAME(userReadCurrentlyPlaying) = (1 << 17),
67
- /// Read access to a user’s currently playing track
68
- SPTUserReadRecentlyPlayedScope NS_SWIFT_NAME(userReadRecentlyPlayed) = (1 << 18),
69
- /// Indicate that the application intends to use OIDC to verify the user's identity
70
- SPTOpenIdScope NS_SWIFT_NAME(openid) = (1 << 19),
71
- };
33
+ /// Include collaborative playlists when requesting a user's playlists.
34
+ SPTPlaylistReadCollaborativeScope NS_SWIFT_NAME(playlistReadCollaborative) = (1 << 1),
35
+ /// Write access to a user's public playlists.
36
+ SPTPlaylistModifyPublicScope NS_SWIFT_NAME(playlistModifyPublic) = (1 << 2),
37
+ /// Write access to a user's private playlists.
38
+ SPTPlaylistModifyPrivateScope NS_SWIFT_NAME(playlistModifyPrivate) = (1 << 3),
39
+ /// Read access to the list of artists and other users that the user follows.
40
+ SPTUserFollowReadScope NS_SWIFT_NAME(userFollowRead) = (1 << 4),
41
+ /// Write/delete access to the list of artists and other users that the user follows.
42
+ SPTUserFollowModifyScope NS_SWIFT_NAME(userFollowModify) = (1 << 5),
43
+ /// Read access to a user's "Your Music" library.
44
+ SPTUserLibraryReadScope NS_SWIFT_NAME(userLibraryRead) = (1 << 6),
45
+ /// Write/delete access to a user's "Your Music" library.
46
+ SPTUserLibraryModifyScope NS_SWIFT_NAME(userLibraryModify) = (1 << 7),
47
+ /// Read access to the user's birthdate.
48
+ SPTUserReadBirthDateScope NS_SWIFT_NAME(userReadBirthDate) = (1 << 8),
49
+ /// Read access to user’s email address.
50
+ SPTUserReadEmailScope NS_SWIFT_NAME(userReadEmail) = (1 << 9),
51
+ /// Read access to user’s subscription details (type of user account).
52
+ SPTUserReadPrivateScope NS_SWIFT_NAME(userReadPrivate) = (1 << 10),
53
+ /// Read access to a user's top artists and tracks.
54
+ SPTUserTopReadScope NS_SWIFT_NAME(userTopRead) = (1 << 11),
55
+ /// Upload user generated content images
56
+ SPTUGCImageUploadScope NS_SWIFT_NAME(ugcImageUpload) = (1 << 12),
57
+ /// Control playback of a Spotify track.
58
+ SPTStreamingScope NS_SWIFT_NAME(streaming) = (1 << 13),
59
+ /// Use App Remote to control playback in the Spotify app
60
+ SPTAppRemoteControlScope NS_SWIFT_NAME(appRemoteControl) = (1 << 14),
61
+ /// Read access to a user’s player state.
62
+ SPTUserReadPlaybackStateScope NS_SWIFT_NAME(userReadPlaybackState) = (1 << 15),
63
+ /// Write access to a user’s playback state
64
+ SPTUserModifyPlaybackStateScope NS_SWIFT_NAME(userModifyPlaybackState) = (1 << 16),
65
+ /// Read access to a user’s currently playing
66
+ /// track
67
+ SPTUserReadCurrentlyPlayingScope NS_SWIFT_NAME(userReadCurrentlyPlaying) = (1 << 17),
68
+ /// Read access to a user’s currently
69
+ /// playing track
70
+ SPTUserReadRecentlyPlayedScope NS_SWIFT_NAME(userReadRecentlyPlayed) = (1 << 18),
71
+ /// Indicate that the application
72
+ /// intends to use OIDC to verify the
73
+ /// user's identity
74
+ SPTOpenIdScope NS_SWIFT_NAME(openid) = (1 << 19),
75
+ };
@@ -8,11 +8,15 @@ NS_ASSUME_NONNULL_BEGIN
8
8
 
9
9
  /// Options for determining the most suitable method for authorization
10
10
  typedef NS_OPTIONS(NSUInteger, SPTAuthorizationOptions) {
11
- /// Authorize using a suitable method. If Spotify is installed the app will be used instead of a web view
11
+ /// Authorize using a suitable method. If Spotify is installed the app will be used instead of ASWebAuthentication.
12
12
  SPTDefaultAuthorizationOption NS_SWIFT_NAME(default) = (0),
13
- /// Authorize using only the Spotify client. If Spotify is not installed authorization will fail.
14
- SPTClientAuthorizationOption NS_SWIFT_NAME(clientOnly) = (1 << 0),
15
- } NS_SWIFT_NAME(AuthorizationOptions);
13
+ /// Authorize using only the Spotify client. If Spotify is not installed authorization will fail.
14
+ SPTClientAuthorizationOption NS_SWIFT_NAME(clientOnly) = (1 << 0),
15
+ /// Use this option if you have reached the limit of `LSApplicationQueriesSchemes` in your Info.plist
16
+ /// Attempt to open Spotify app directly without using `canOpenURL`. Falls back to ASWebAuthentication if
17
+ /// opening fails.
18
+ SPTSpotifySchemeNotRegisteredAuthorizationOption NS_SWIFT_NAME(spotifySchemeNotRegistered) = (1 << 1),
19
+ } NS_SWIFT_NAME(AuthorizationOptions);
16
20
 
17
21
  typedef NSString *const SPTAuthorizationCode;
18
22
 
@@ -4,4 +4,4 @@
4
4
  #import "SPTLogin.h"
5
5
  #import "SpotifyAppRemote.h"
6
6
 
7
- #endif /* _SPOTIFYIOS_ */
7
+ #endif /* _SPOTIFYIOS_ */
@@ -0,0 +1,118 @@
1
+ import Foundation
2
+
3
+ enum SpotifyRefreshError: Error {
4
+ case invalidURL(String)
5
+ case network(Error)
6
+ case http(status: Int, body: String?)
7
+ case parse(String)
8
+
9
+ var code: String {
10
+ switch self {
11
+ case .invalidURL: return "INVALID_CONFIG"
12
+ case .network: return "NETWORK_ERROR"
13
+ case .http: return "TOKEN_SWAP_FAILED"
14
+ case .parse: return "TOKEN_SWAP_PARSE_ERROR"
15
+ }
16
+ }
17
+
18
+ var message: String {
19
+ switch self {
20
+ case .invalidURL(let s):
21
+ return "Invalid token refresh URL: \(s)"
22
+ case .network(let err):
23
+ return err.localizedDescription
24
+ case .http(let status, let body):
25
+ let trimmed = body.map { String($0.prefix(512)) } ?? ""
26
+ return "Token refresh server returned HTTP \(status)\(trimmed.isEmpty ? "" : ": \(trimmed)")"
27
+ case .parse(let m):
28
+ return m
29
+ }
30
+ }
31
+ }
32
+
33
+ struct SpotifyRefreshResult {
34
+ let accessToken: String
35
+ let refreshToken: String?
36
+ let expirationDate: Int64
37
+ let scopes: [String]
38
+ }
39
+
40
+ struct SpotifyTokenRefreshClient {
41
+ let sdkVersion: String
42
+ let clientID: String
43
+ let session: URLSession
44
+
45
+ init(sdkVersion: String, clientID: String, session: URLSession = .shared) {
46
+ self.sdkVersion = sdkVersion
47
+ self.clientID = clientID
48
+ self.session = session
49
+ }
50
+
51
+ func refresh(
52
+ refreshToken: String,
53
+ tokenRefreshURL urlString: String,
54
+ previousScopes: [String]
55
+ ) async throws -> SpotifyRefreshResult {
56
+ guard let url = URL(string: urlString) else {
57
+ throw SpotifyRefreshError.invalidURL(urlString)
58
+ }
59
+ var request = URLRequest(url: url)
60
+ request.httpMethod = "POST"
61
+ request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
62
+ request.setValue("expo-spotify-sdk/\(sdkVersion)", forHTTPHeaderField: "User-Agent")
63
+ request.httpBody = formURLEncoded([
64
+ "refresh_token": refreshToken,
65
+ "client_id": clientID,
66
+ ]).data(using: .utf8)
67
+
68
+ let data: Data
69
+ let response: URLResponse
70
+ do {
71
+ (data, response) = try await session.data(for: request)
72
+ } catch {
73
+ throw SpotifyRefreshError.network(error)
74
+ }
75
+
76
+ guard let http = response as? HTTPURLResponse else {
77
+ throw SpotifyRefreshError.parse("Non-HTTP response from refresh endpoint")
78
+ }
79
+ guard (200..<300).contains(http.statusCode) else {
80
+ let body = String(data: data, encoding: .utf8)
81
+ throw SpotifyRefreshError.http(status: http.statusCode, body: body)
82
+ }
83
+
84
+ let json: [String: Any]
85
+ do {
86
+ guard let parsed = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
87
+ throw SpotifyRefreshError.parse("Refresh response was not a JSON object")
88
+ }
89
+ json = parsed
90
+ } catch {
91
+ throw SpotifyRefreshError.parse("Refresh response was not valid JSON: \(error.localizedDescription)")
92
+ }
93
+
94
+ guard let accessToken = (json["access_token"] as? String), !accessToken.isEmpty else {
95
+ throw SpotifyRefreshError.parse("Refresh response missing required field: access_token")
96
+ }
97
+ guard let expiresIn = json["expires_in"] as? Int else {
98
+ throw SpotifyRefreshError.parse("Refresh response missing required field: expires_in")
99
+ }
100
+ let rotatedRefreshToken = (json["refresh_token"] as? String).flatMap { $0.isEmpty ? nil : $0 }
101
+ let scopeString = json["scope"] as? String
102
+ let scopes = scopeString?.split(separator: " ").map(String.init) ?? previousScopes
103
+ let expirationDate = Int64(Date().timeIntervalSince1970 * 1000) + Int64(expiresIn) * 1000
104
+
105
+ return SpotifyRefreshResult(
106
+ accessToken: accessToken,
107
+ refreshToken: rotatedRefreshToken ?? refreshToken,
108
+ expirationDate: expirationDate,
109
+ scopes: scopes
110
+ )
111
+ }
112
+
113
+ private func formURLEncoded(_ pairs: [String: String]) -> String {
114
+ var components = URLComponents()
115
+ components.queryItems = pairs.map { URLQueryItem(name: $0.key, value: $0.value) }
116
+ return components.percentEncodedQuery ?? ""
117
+ }
118
+ }
package/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@wwdrew/expo-spotify-sdk",
3
- "version": "0.5.0",
4
- "description": "Expo Module for native Spotify SDK",
3
+ "version": "0.7.0",
4
+ "description": "Expo module wrapping the native Spotify iOS (v5) and Android (v4) SDKs for OAuth authentication",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "expo-module build",
9
9
  "clean": "expo-module clean",
10
10
  "lint": "expo-module lint",
11
- "test": "expo-module test",
11
+ "test": "expo-module test plugin",
12
12
  "prepare": "expo-module prepare",
13
13
  "prepublishOnly": "expo-module prepublishOnly",
14
+ "typecheck": "expo-module typecheck",
14
15
  "expo-module": "expo-module",
15
16
  "open:ios": "open -a \"Xcode\" example/ios",
16
17
  "open:android": "open -a \"Android Studio\" example/android"
@@ -28,23 +29,23 @@
28
29
  "android",
29
30
  "ios",
30
31
  "app.plugin.js",
31
- "expo-module.config.json",
32
- "server.js"
32
+ "expo-module.config.json"
33
33
  ],
34
- "jest": {
35
- "preset": "expo-module-scripts"
36
- },
37
34
  "repository": "https://github.com/wwdrew/expo-spotify-sdk",
38
35
  "bugs": {
39
36
  "url": "https://github.com/wwdrew/expo-spotify-sdk/issues"
40
37
  },
41
- "author": "Drew Miller <49833875+wwdrew@users.noreply.github.com> (https:/github.com/wwdrew/)",
38
+ "author": "Drew Miller <49833875+wwdrew@users.noreply.github.com> (https://github.com/wwdrew/)",
42
39
  "license": "MIT",
43
40
  "homepage": "https://github.com/wwdrew/expo-spotify-sdk#readme",
44
41
  "devDependencies": {
42
+ "@expo/config-plugins": "^55.0.8",
45
43
  "@types/react": "^18.0.25",
46
- "expo-module-scripts": "^3.1.0",
47
- "expo-modules-core": "^1.5.11"
44
+ "eslint": "^9.0.0",
45
+ "expo-module-scripts": "^55.0.2",
46
+ "expo-modules-core": "^3.0.30",
47
+ "jest": "^29.7.0",
48
+ "prettier": "^3.0.0"
48
49
  },
49
50
  "peerDependencies": {
50
51
  "expo": "*",
@@ -2,27 +2,32 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.withSpotifyAndroidAppBuildGradle = void 0;
4
4
  const config_plugins_1 = require("@expo/config-plugins");
5
+ const SENTINEL_KEY = "spotifyClientId";
6
+ const DEFAULT_REDIRECT_PATH_PATTERN = "/.*";
5
7
  const withSpotifyAndroidAppBuildGradle = (config, spotifyConfig) => {
6
8
  return (0, config_plugins_1.withAppBuildGradle)(config, (config) => {
7
- const defaultConfigPattern = /(defaultConfig\s*{[\s\S]*?)(})/s;
8
- const manifestPlaceholders = `
9
- manifestPlaceholders = [
9
+ if (config.modResults.contents.includes(SENTINEL_KEY)) {
10
+ // Already injected on a previous prebuild; bail to keep this modifier
11
+ // idempotent. Run `expo prebuild --clean` to force a refresh.
12
+ return config;
13
+ }
14
+ const redirectPathPattern = spotifyConfig.redirectPathPattern ?? DEFAULT_REDIRECT_PATH_PATTERN;
15
+ const placeholders = ` manifestPlaceholders = [
10
16
  spotifyClientId: "${spotifyConfig.clientID}",
11
17
  spotifyRedirectUri: "${spotifyConfig.scheme}://${spotifyConfig.host}",
12
18
  redirectSchemeName: "${spotifyConfig.scheme}",
13
- redirectHostName: "${spotifyConfig.host}"
14
- ]
15
- `;
19
+ redirectHostName: "${spotifyConfig.host}",
20
+ redirectPathPattern: "${redirectPathPattern}"
21
+ ]`;
22
+ const defaultConfigPattern = /(defaultConfig\s*\{[\s\S]*?)(\n\s*})/m;
16
23
  if (defaultConfigPattern.test(config.modResults.contents)) {
17
- // If the defaultConfig block exists, add the manifestPlaceholders to it
18
- config.modResults.contents = config.modResults.contents.replace(defaultConfigPattern, `$1${manifestPlaceholders}$2`);
24
+ config.modResults.contents = config.modResults.contents.replace(defaultConfigPattern, `$1\n${placeholders}$2`);
19
25
  }
20
26
  else {
21
- // If the defaultConfig block doesn't exist, add it to the android block
22
27
  config.modResults.contents += `
23
28
  android {
24
29
  defaultConfig {
25
- ${manifestPlaceholders}
30
+ ${placeholders}
26
31
  }
27
32
  }`;
28
33
  }
@@ -2,15 +2,20 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.withSpotifyConfigValues = void 0;
4
4
  const config_plugins_1 = require("@expo/config-plugins");
5
+ const IOS_KEYS = [
6
+ "clientID",
7
+ "host",
8
+ "scheme",
9
+ ];
5
10
  const withSpotifyConfigValues = (config, spotifyConfig) => (0, config_plugins_1.withInfoPlist)(config, (config) => {
6
- if (!config.modResults.ExpoSpotifySDK) {
7
- config.modResults.ExpoSpotifySDK = {};
11
+ const existing = (config.modResults.ExpoSpotifySDK ?? {});
12
+ for (const key of IOS_KEYS) {
13
+ const value = spotifyConfig[key];
14
+ if (typeof value === "string" && value.length > 0) {
15
+ existing[key] = value;
16
+ }
8
17
  }
9
- const spotifySDKConfig = config.modResults
10
- .ExpoSpotifySDK;
11
- Object.entries(spotifyConfig).forEach(([key, value]) => {
12
- spotifySDKConfig[key] = value;
13
- });
18
+ config.modResults.ExpoSpotifySDK = existing;
14
19
  return config;
15
20
  });
16
21
  exports.withSpotifyConfigValues = withSpotifyConfigValues;
@@ -1,3 +1,3 @@
1
- import { ConfigPlugin } from "expo/config-plugins";
1
+ import { ConfigPlugin } from "@expo/config-plugins";
2
2
  import { SpotifyConfig } from "../types";
3
3
  export declare const withSpotifyURLScheme: ConfigPlugin<SpotifyConfig>;
@@ -1,18 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.withSpotifyURLScheme = void 0;
4
- const config_plugins_1 = require("expo/config-plugins");
4
+ const config_plugins_1 = require("@expo/config-plugins");
5
5
  const withSpotifyURLScheme = (config, { scheme }) => {
6
6
  return (0, config_plugins_1.withInfoPlist)(config, (config) => {
7
7
  const bundleId = config.ios?.bundleIdentifier;
8
- const urlType = {
9
- CFBundleURLName: bundleId,
10
- CFBundleURLSchemes: [scheme],
11
- };
12
- if (!config.modResults.CFBundleURLTypes) {
13
- config.modResults.CFBundleURLTypes = [];
8
+ const urlTypes = config.modResults.CFBundleURLTypes ?? [];
9
+ const alreadyDeclared = urlTypes.some((entry) => entry.CFBundleURLSchemes?.includes(scheme));
10
+ if (!alreadyDeclared) {
11
+ urlTypes.push({
12
+ CFBundleURLName: bundleId ?? "",
13
+ CFBundleURLSchemes: [scheme],
14
+ });
15
+ config.modResults.CFBundleURLTypes = urlTypes;
14
16
  }
15
- config.modResults.CFBundleURLTypes.push(urlType);
16
17
  return config;
17
18
  });
18
19
  };
@@ -1,6 +1,41 @@
1
+ /**
2
+ * Spotify scopes that are valid in Spotify's iOS, Android and Web auth APIs.
3
+ *
4
+ * See: https://developer.spotify.com/documentation/web-api/concepts/scopes
5
+ */
1
6
  export type SpotifyScopes = "ugc-image-upload" | "user-read-playback-state" | "user-modify-playback-state" | "user-read-currently-playing" | "app-remote-control" | "streaming" | "playlist-read-private" | "playlist-read-collaborative" | "playlist-modify-private" | "playlist-modify-public" | "user-follow-modify" | "user-follow-read" | "user-top-read" | "user-read-recently-played" | "user-library-modify" | "user-library-read" | "user-read-email" | "user-read-private";
7
+ /**
8
+ * Configuration accepted by the `@wwdrew/expo-spotify-sdk` Expo config plugin.
9
+ *
10
+ * @example
11
+ * // app.config.ts
12
+ * export default {
13
+ * plugins: [
14
+ * ["@wwdrew/expo-spotify-sdk", {
15
+ * clientID: "<spotify-client-id>",
16
+ * scheme: "myapp",
17
+ * host: "spotify-auth",
18
+ * redirectPathPattern: "/.*"
19
+ * }]
20
+ * ]
21
+ * }
22
+ */
2
23
  export interface SpotifyConfig {
24
+ /** Spotify Client ID for your application. */
3
25
  clientID: string;
26
+ /** Path component of the redirect URI (e.g. `"spotify-auth"`). */
4
27
  host: string;
28
+ /**
29
+ * URL scheme registered for your app, used as the redirect URI scheme
30
+ * (e.g. `"myapp"`). Must match your Expo app's `scheme`.
31
+ */
5
32
  scheme: string;
33
+ /**
34
+ * Path pattern Spotify will accept on the redirect URI. Required by the
35
+ * Spotify Android Auth SDK from version 3.0.0 onwards. Defaults to `"/.*"`
36
+ * which matches any path (preserving pre-3.0.0 SDK behaviour).
37
+ *
38
+ * See: https://developer.android.com/guide/topics/manifest/data-element#path
39
+ */
40
+ redirectPathPattern?: string;
6
41
  }
@@ -1,21 +1,2 @@
1
1
  "use strict";
2
- /*
3
-
4
- List of Scopes: https://developer.spotify.com/documentation/web-api/concepts/scopes
5
-
6
- Note: these scopes are not currently available in the iOS SDK:
7
-
8
- - user-read-playback-position
9
- - user-soa-link
10
- - user-soa-unlink
11
- - user-manage-entitlements
12
- - user-manage-partner
13
- - user-create-partner
14
-
15
- Also, although these scopes exist in the iOS SDK, they are not valid:
16
-
17
- - user-read-birthdate
18
- - openid
19
-
20
- */
21
2
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,13 +0,0 @@
1
- import ExpoModulesCore
2
- import SpotifyiOS
3
-
4
- public class ExpoSpotifyAuthDelegate: ExpoAppDelegateSubscriber {
5
- let sessionManager = ExpoSpotifySessionManager.shared
6
-
7
- public func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
8
- if let canHandleURL = sessionManager.sessionManager?.application(app, open: url, options: options) {
9
- return canHandleURL
10
- }
11
- return false
12
- }
13
- }
@@ -1,16 +0,0 @@
1
- import Foundation
2
- import SpotifyiOS
3
-
4
- extension ExpoSpotifySessionManager: SPTSessionManagerDelegate {
5
- public func sessionManager(manager _: SPTSessionManager, didInitiate session: SPTSession) {
6
- authPromiseSeal?.fulfill(session)
7
- }
8
-
9
- public func sessionManager(manager _: SPTSessionManager, didFailWith error: Error) {
10
- authPromiseSeal?.reject(error)
11
- }
12
-
13
- public func sessionManager(manager _: SPTSessionManager, didRenew session: SPTSession) {
14
- authPromiseSeal?.fulfill(session)
15
- }
16
- }
@@ -1,90 +0,0 @@
1
- import ExpoModulesCore
2
- import SpotifyiOS
3
- import PromiseKit
4
-
5
- enum SessionManagerError: Error {
6
- case notInitialized
7
- case invalidConfiguration
8
- }
9
-
10
- final class ExpoSpotifySessionManager: NSObject {
11
- weak var module: ExpoSpotifySDKModule?
12
- var authPromiseSeal: Resolver<SPTSession>?
13
-
14
- static let shared = ExpoSpotifySessionManager()
15
-
16
- private var expoSpotifyConfiguration: ExpoSpotifyConfiguration? {
17
- guard let expoSpotifySdkDict = Bundle.main.object(forInfoDictionaryKey: "ExpoSpotifySDK") as? [String: String],
18
- let clientID = expoSpotifySdkDict["clientID"],
19
- let host = expoSpotifySdkDict["host"],
20
- let scheme = expoSpotifySdkDict["scheme"] else
21
- {
22
- return nil
23
- }
24
-
25
- return ExpoSpotifyConfiguration(clientID: clientID, host: host, scheme: scheme)
26
- }
27
-
28
- lazy var configuration: SPTConfiguration? = {
29
- guard let clientID = expoSpotifyConfiguration?.clientID,
30
- let redirectURL = expoSpotifyConfiguration?.redirectURL else {
31
- NSLog("Invalid Spotify configuration")
32
- return nil
33
- }
34
-
35
- return SPTConfiguration(clientID: clientID, redirectURL: redirectURL)
36
- }()
37
-
38
- lazy var sessionManager: SPTSessionManager? = {
39
- guard let configuration = configuration else {
40
- return nil
41
- }
42
-
43
- return SPTSessionManager(configuration: configuration, delegate: self)
44
- }()
45
-
46
-
47
- func authenticate(scopes: [String], tokenSwapURL: String?, tokenRefreshURL: String?) -> PromiseKit.Promise<SPTSession> {
48
- return Promise { seal in
49
- guard let clientID = self.expoSpotifyConfiguration?.clientID,
50
- let redirectURL = self.expoSpotifyConfiguration?.redirectURL else {
51
- NSLog("Invalid Spotify configuration")
52
- seal.reject(SessionManagerError.invalidConfiguration)
53
- return
54
- }
55
-
56
- let configuration = SPTConfiguration(clientID: clientID, redirectURL: redirectURL)
57
-
58
- if (tokenSwapURL != nil) {
59
- configuration.tokenSwapURL = URL(string: tokenSwapURL ?? "")
60
- }
61
-
62
- if (tokenRefreshURL != nil) {
63
- configuration.tokenRefreshURL = URL(string: tokenRefreshURL ?? "")
64
- }
65
-
66
- self.authPromiseSeal = seal
67
- self.configuration = configuration
68
- self.sessionManager = SPTSessionManager(configuration: configuration, delegate: self)
69
-
70
- DispatchQueue.main.sync {
71
- sessionManager?.initiateSession(with: SPTScopeSerializer.deserializeScopes(scopes), options: .default, campaign: nil)
72
- }
73
- }
74
- }
75
-
76
- func spotifyAppInstalled() -> Bool {
77
- guard let sessionManager = sessionManager else {
78
- NSLog("SPTSessionManager not initialized")
79
- return false
80
- }
81
-
82
- var isInstalled = false
83
-
84
- DispatchQueue.main.sync {
85
- isInstalled = sessionManager.isSpotifyAppInstalled
86
- }
87
-
88
- return isInstalled
89
- }
90
- }