@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
package/LICENSE CHANGED
@@ -1,3 +1,21 @@
1
- {
2
- "license": "MIT"
3
- }
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Andrew Miller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,154 +1,404 @@
1
1
  # expo-spotify-sdk
2
2
 
3
- An Expo Module for the native [iOS](https://github.com/spotify/ios-sdk/) and [Android](https://github.com/spotify/android-sdk/) Spotify SDK
3
+ [![npm version](https://img.shields.io/npm/v/@wwdrew/expo-spotify-sdk)](https://www.npmjs.com/package/@wwdrew/expo-spotify-sdk)
4
+ [![CI](https://img.shields.io/github/actions/workflow/status/wwdrew/expo-spotify-sdk/ci.yml?label=CI)](https://github.com/wwdrew/expo-spotify-sdk/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/github/license/wwdrew/expo-spotify-sdk)](LICENSE)
4
6
 
5
- ## Supported Features
7
+ An Expo module that wraps the native [Spotify iOS SDK](https://github.com/spotify/ios-sdk) (v5.0.1) and [Spotify Android SDK](https://github.com/spotify/android-sdk) (v4.0.1) to provide OAuth authentication in Expo and React Native apps.
6
8
 
7
- - Authentication
9
+ **Why this exists:** Spotify ships native SDKs for iOS and Android that enable authentication via the installed Spotify app (no browser redirect, better UX) but there is no maintained Expo module for them. This library fills that gap.
8
10
 
9
- More to come...
11
+ ## Platform support
10
12
 
11
- ## Installation
13
+ | Feature | iOS | Android | Web |
14
+ | --------------------------------------------- | --- | ------------ | ------------------- |
15
+ | `isAvailable()` | ✅ | ✅ | ✅ (always `false`) |
16
+ | `authenticateAsync` — CODE flow (recommended) | ✅ | ✅ | — |
17
+ | `authenticateAsync` — TOKEN flow (implicit) | ✅ | ⚠️ see below | — |
18
+ | `refreshSessionAsync` | ✅ | ✅ | — |
19
+ | Auth via installed Spotify app | ✅ | ✅ | — |
20
+ | Auth via Spotify web fallback | ✅ | ✅ | — |
21
+
22
+ ## Quick start (Expo)
12
23
 
13
24
  ```sh
25
+ # 1. Install
14
26
  npx expo install @wwdrew/expo-spotify-sdk
27
+
28
+ # 2. Add the config plugin to app.config.ts / app.json (see Configuration below)
29
+ # 3. Regenerate native projects
30
+ npx expo prebuild
15
31
  ```
16
32
 
17
- ## Configuration
33
+ For bare React Native (no Expo CLI), see [Installation in bare React Native](#installation-in-bare-react-native).
34
+
35
+ ## Installation in bare React Native
36
+
37
+ This library is an [Expo Module](https://docs.expo.dev/modules/overview/) and therefore requires `expo-modules-core` as a peer. If your project does not use the Expo managed workflow you will need to set this up manually.
38
+
39
+ ### 1. Install
40
+
41
+ ```sh
42
+ npm install @wwdrew/expo-spotify-sdk expo-modules-core
43
+ # or
44
+ yarn add @wwdrew/expo-spotify-sdk expo-modules-core
45
+ ```
46
+
47
+ ### 2. iOS
48
+
49
+ Add the pod to your `Podfile`:
50
+
51
+ ```ruby
52
+ pod 'ExpoSpotifySDK', :path => '../node_modules/@wwdrew/expo-spotify-sdk'
53
+ ```
54
+
55
+ Then install pods:
56
+
57
+ ```sh
58
+ cd ios && pod install
59
+ ```
60
+
61
+ If you have not already bootstrapped `expo-modules-core` in your AppDelegate, follow the [Expo Modules integration guide](https://docs.expo.dev/bare/installing-expo-modules/) first — in particular, your `AppDelegate` must inherit from `ExpoAppDelegate` (or call `ExpoModulesAppDelegateSubscriber`) so that the Spotify redirect URL is handled correctly.
62
+
63
+ Finally, register your URL scheme. In Xcode open **Info → URL Types** and add a new entry with:
64
+
65
+ - **Identifier:** `$(PRODUCT_BUNDLE_IDENTIFIER)`
66
+ - **URL Schemes:** the value you'll pass as `scheme` in the plugin config (e.g. `myapp`)
67
+
68
+ ### 3. Android
69
+
70
+ You do **not** need to modify `AndroidManifest.xml`. The module's own manifest (merged by Gradle at build time) already contributes the `<queries>` block for package-visibility and the `<meta-data>` placeholders. The Spotify Auth SDK's AAR brings in its own activities the same way.
71
+
72
+ The only manual step is in `android/app/build.gradle`. Add the Spotify Auth SDK dependency and populate the manifest placeholders that the module expects:
18
73
 
19
- Include the `expo-spotify-sdk` plugin in your `app.json/app.config.js` file with its required configuration:
74
+ ```groovy
75
+ android {
76
+ defaultConfig {
77
+ // ...
78
+ manifestPlaceholders = [
79
+ spotifyClientId: "your-spotify-client-id",
80
+ spotifyRedirectUri: "myapp://spotify-auth",
81
+ redirectSchemeName: "myapp",
82
+ redirectHostName: "spotify-auth",
83
+ redirectPathPattern: "/.*"
84
+ ]
85
+ }
86
+ }
87
+
88
+ dependencies {
89
+ // ...
90
+ implementation 'com.spotify.android:auth:4.0.1'
91
+ implementation 'com.squareup.okhttp3:okhttp:4.12.0' // required for token swap/refresh
92
+ }
93
+ ```
94
+
95
+ Replace `myapp`, `spotify-auth`, and `your-spotify-client-id` with your own values. Make sure the redirect URI matches what is registered in your [Spotify Developer Dashboard](https://developer.spotify.com/dashboard).
96
+
97
+ ## Configuration
20
98
 
21
- ```javascript
22
- ...
23
- "plugins": [
24
- ["@wwdrew/expo-spotify-sdk", {
25
- "clientID": "<your-spotify-client-id>",
26
- "scheme": "expo-spotify-sdk-example",
27
- "host": "authenticate"
28
- }]
99
+ Add the plugin to your `app.config.ts` (or `app.json`):
100
+
101
+ ```ts
102
+ export default {
103
+ plugins: [
104
+ [
105
+ "@wwdrew/expo-spotify-sdk",
106
+ {
107
+ clientID: "your-spotify-client-id",
108
+ scheme: "myapp",
109
+ host: "spotify-auth",
110
+ // Optional: path pattern accepted by the Spotify Android SDK redirect.
111
+ // Defaults to "/.*" (matches any path). Change this only if you have a
112
+ // specific redirect path registered in your Spotify app settings.
113
+ redirectPathPattern: "/.*",
114
+ },
115
+ ],
29
116
  ],
30
- ...
117
+ };
31
118
  ```
32
119
 
33
- Required:
120
+ ### Plugin options
121
+
122
+ | Option | Type | Required | Description |
123
+ | --------------------- | -------- | -------- | ---------------------------------------------------------- |
124
+ | `clientID` | `string` | ✅ | Your Spotify application's Client ID |
125
+ | `scheme` | `string` | ✅ | URL scheme registered for your app (e.g. `"myapp"`) |
126
+ | `host` | `string` | ✅ | Host component of the redirect URI (e.g. `"spotify-auth"`) |
127
+ | `redirectPathPattern` | `string` | — | Android redirect path regex. Defaults to `"/.*"` |
128
+
129
+ The redirect URI registered in your [Spotify Developer Dashboard](https://developer.spotify.com/dashboard) must match `{scheme}://{host}` exactly (e.g. `myapp://spotify-auth`).
130
+
131
+ ## Usage
132
+
133
+ ```ts
134
+ import {
135
+ isAvailable,
136
+ authenticateAsync,
137
+ refreshSessionAsync,
138
+ addSessionChangeListener,
139
+ SpotifyError,
140
+ } from "@wwdrew/expo-spotify-sdk";
141
+
142
+ // Check whether the Spotify app is installed
143
+ const spotifyInstalled = isAvailable();
144
+
145
+ // Authenticate
146
+ try {
147
+ const session = await authenticateAsync({
148
+ scopes: ["user-read-email", "streaming"],
149
+ tokenSwapURL: "https://your-server.example.com/swap",
150
+ tokenRefreshURL: "https://your-server.example.com/refresh",
151
+ });
152
+
153
+ console.log(session.accessToken); // use with Spotify Web API
154
+ console.log(session.refreshToken); // store securely for later refresh
155
+ console.log(session.expirationDate); // Unix epoch milliseconds
156
+ console.log(session.scopes); // granted scopes
157
+ } catch (e) {
158
+ if (e instanceof SpotifyError) {
159
+ if (e.code === "USER_CANCELLED") return; // user backed out — benign
160
+ console.error(`[${e.code}] ${e.message}`);
161
+ }
162
+ }
163
+ ```
34
164
 
35
- - `clientID`: &lt;string&gt; the Spotify Client ID for your application
36
- - `scheme`: &lt;string&gt; the [URL scheme](https://docs.expo.dev/versions/latest/config/app/#scheme) to link into your app as part of the redirect URI
37
- - `host`: &lt;string&gt; the path of the redirect URI
165
+ ### Refresh a session
38
166
 
39
- ## API Reference
167
+ ```ts
168
+ import { refreshSessionAsync } from "@wwdrew/expo-spotify-sdk";
40
169
 
41
- ```typescript
42
- isAvailable(): boolean`
170
+ const refreshed = await refreshSessionAsync({
171
+ refreshToken: storedRefreshToken,
172
+ tokenRefreshURL: "https://your-server.example.com/refresh",
173
+ });
43
174
  ```
44
175
 
45
- Determines if the Spotify app is installed on the target device.
176
+ ## API reference
177
+
178
+ ### `isAvailable(): boolean`
179
+
180
+ Returns `true` if the Spotify app is installed on the device. Always returns `false` on web. Does not throw.
46
181
 
47
182
  ---
48
183
 
49
- ```typescript
50
- authenticateAsync(config: SpotifyConfig): Promise<SpotifySession>
51
- ```
184
+ ### `authenticateAsync(config: SpotifyConfig): Promise<SpotifySession>`
52
185
 
53
- Starts the authentication process. Requires an array of OAuth scopes. If the Spotify app is installed on the target device it will interact directly with it, otherwise it will open a web view to authenticate with the Spotify website.
186
+ Starts a Spotify OAuth flow. If the Spotify app is installed it authenticates natively; otherwise it falls back to the Spotify web login page.
54
187
 
55
- **Note for Android:** If not providing a token swap or refresh URL, the Spotify session response access token will expire after 60 minutes and will not include a refresh token. This is due to a limitation in the Android Spotify SDK. It's recommended to [implement a token swap endpoint](#token-swap) for this reason.
188
+ **Throws [`SpotifyError`](#spotifyerror)** on failure.
56
189
 
57
- ### Parameters
190
+ **Parameters (`SpotifyConfig`):**
58
191
 
59
- - `tokenSwapURL` (optional): &lt;string&gt; The URL to use for attempting to swap an authorization code for an access token
60
- - `tokenRefreshURL` (optional): &lt;string&gt; The URL to use for attempting to renew an access token with a refresh token
61
- - `scopes`: An array of OAuth scopes that declare how your app wants to access a user's account. See [Spotify Scopes](https://developer.spotify.com/web-api/using-scopes/) for more information.
192
+ | Field | Type | Required | Description |
193
+ | ----------------- | ---------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
194
+ | `scopes` | `SpotifyScope[]` | ✅ | OAuth scopes to request. Must contain at least one entry. |
195
+ | `tokenSwapURL` | `string` | — | URL of your token swap server endpoint. Triggers CODE flow (recommended). Required on Android to receive a `refreshToken`. |
196
+ | `tokenRefreshURL` | `string` | — | URL of your token refresh server endpoint. Used by iOS SDK natively and by `refreshSessionAsync` on both platforms. |
197
+ | `showDialog` | `boolean` | — | Force Spotify to show the authorization dialog even when the user already has an active session. Defaults to `false`. Useful during development; avoid in production. |
62
198
 
63
- **Note:** The following scopes are not available to Expo Spotify SDK:
199
+ **Returns (`SpotifySession`):**
64
200
 
65
- - user-read-playback-position
66
- - user-soa-link
67
- - user-soa-unlink
68
- - user-manage-entitlements
69
- - user-manage-partner
70
- - user-create-partner
201
+ | Field | Type | Description |
202
+ | ---------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
203
+ | `accessToken` | `string` | OAuth access token. Use as `Authorization: Bearer <token>` with the Spotify Web API. |
204
+ | `refreshToken` | `string \| null` | Refresh token. `null` on Android when no `tokenSwapURL` is provided — see [Android implicit flow](#android-implicit-token-flow-is-not-recommended). |
205
+ | `expirationDate` | `number` | Token expiry as Unix epoch **milliseconds**. |
206
+ | `scopes` | `SpotifyScope[]` | Granted scopes. On Android TOKEN flow, reflects requested scopes (Spotify does not expose granted scopes in the implicit flow). |
71
207
 
72
- ### Types
208
+ ---
73
209
 
74
- ```typescript
75
- interface SpotifyConfig {
76
- scopes: SpotifyScope[];
77
- tokenSwapURL?: string;
78
- tokenRefreshURL?: string;
79
- }
210
+ ### `refreshSessionAsync(options): Promise<SpotifySession>`
211
+
212
+ Exchanges a refresh token for a new access token via your token refresh server.
213
+
214
+ **Parameters:**
215
+
216
+ | Field | Type | Description |
217
+ | ----------------- | -------- | ----------------------------------------------------------- |
218
+ | `refreshToken` | `string` | The refresh token from a previous `authenticateAsync` call. |
219
+ | `tokenRefreshURL` | `string` | URL of your token refresh server endpoint. |
220
+
221
+ **Returns** a fresh `SpotifySession` with an updated `accessToken` and `expirationDate`. If the server rotates the refresh token the new one is returned in `refreshToken`; otherwise the original token is returned so you can continue refreshing.
222
+
223
+ ---
224
+
225
+ ### `addSessionChangeListener(listener): Subscription`
226
+
227
+ Subscribes to session lifecycle events emitted by the native module. Events fire for every `authenticateAsync` and `refreshSessionAsync` call — including ones you didn't directly `await` — making this the right place to persist tokens in a central store.
80
228
 
81
- interface SpotifySession {
82
- accessToken: string;
83
- refreshToken: string | null;
84
- expirationDate: number;
85
- scopes: SpotifyScopes[];
229
+ Returns a `Subscription` object; call `.remove()` to unsubscribe.
230
+
231
+ ```ts
232
+ import { addSessionChangeListener } from "@wwdrew/expo-spotify-sdk";
233
+
234
+ const sub = addSessionChangeListener((event) => {
235
+ switch (event.type) {
236
+ case "didInitiate":
237
+ case "didRenew":
238
+ store.setSession(event.session); // { accessToken, refreshToken, expirationDate, scopes }
239
+ break;
240
+ case "didFail":
241
+ console.error(`[${event.error.code}] ${event.error.message}`);
242
+ break;
243
+ }
244
+ });
245
+
246
+ // When the subscribing component unmounts:
247
+ sub.remove();
248
+ ```
249
+
250
+ **Event types (`SpotifySessionChangeEvent`):**
251
+
252
+ | `type` | Payload | Fired when |
253
+ |---|---|---|
254
+ | `"didInitiate"` | `{ session: SpotifySession }` | `authenticateAsync` succeeded |
255
+ | `"didRenew"` | `{ session: SpotifySession }` | `refreshSessionAsync` succeeded |
256
+ | `"didFail"` | `{ error: { code, message } }` | Either function rejected |
257
+
258
+ ---
259
+
260
+ ### `SpotifyError`
261
+
262
+ All rejections from `authenticateAsync` and `refreshSessionAsync` are instances of `SpotifyError`:
263
+
264
+ ```ts
265
+ import { SpotifyError } from "@wwdrew/expo-spotify-sdk";
266
+
267
+ try {
268
+ await authenticateAsync({ scopes: ["streaming"] });
269
+ } catch (e) {
270
+ if (e instanceof SpotifyError) {
271
+ switch (e.code) {
272
+ case "USER_CANCELLED": // user closed the auth screen — benign
273
+ case "AUTH_IN_PROGRESS": // concurrent call — benign
274
+ return;
275
+ case "INVALID_CONFIG": // missing clientID / scopes / tokenSwapURL
276
+ case "NETWORK_ERROR": // connectivity failure during token swap
277
+ case "TOKEN_SWAP_FAILED": // swap server returned non-2xx (e.message has status + body)
278
+ case "TOKEN_SWAP_PARSE_ERROR": // swap server returned invalid JSON
279
+ case "SPOTIFY_NOT_INSTALLED": // Spotify app not found (rare — most flows fall back to web)
280
+ case "AUTH_ERROR": // Spotify returned an error (e.message has detail)
281
+ case "UNKNOWN": // unexpected failure
282
+ reportError(e);
283
+ }
284
+ }
86
285
  }
286
+ ```
287
+
288
+ ## Android implicit (TOKEN) flow is not recommended
289
+
290
+ When `authenticateAsync` is called on Android **without** a `tokenSwapURL`, the Spotify Android SDK uses the implicit (TOKEN) flow. This flow has two hard limitations that **will not be fixed** — Spotify has deprecated it:
291
+
292
+ 1. **No `refreshToken`.** The Android SDK does not expose a refresh token for implicit grants. `session.refreshToken` will always be `null`.
293
+ 2. **`scopes` reflects what was requested, not what was granted.** The Android SDK does not return the actual granted scope list for TOKEN responses.
294
+
295
+ The library emits a one-time `console.warn` at runtime when this path is taken.
296
+
297
+ **The fix:** provide a `tokenSwapURL` to use the Authorization Code flow, which returns a full `refreshToken` and the actual granted `scopes` on both platforms.
298
+
299
+ See [Spotify's migration guide](https://developer.spotify.com/documentation/android/tutorials/migration-token-code) for context, and the [token swap server section](#token-swap-server) below for a reference implementation.
300
+
301
+ ## Token swap server
302
+
303
+ The `tokenSwapURL` / `tokenRefreshURL` endpoints must be a server you control — **never** put your Spotify `CLIENT_SECRET` in the app bundle.
304
+
305
+ ### Swap endpoint (`POST {tokenSwapURL}`)
306
+
307
+ The native module sends a `application/x-www-form-urlencoded` body:
87
308
 
88
- type SpotifyScopes =
89
- | "ugc-image-upload"
90
- | "user-read-playback-state"
91
- | "user-modify-playback-state"
92
- | "user-read-currently-playing"
93
- | "app-remote-control"
94
- | "streaming"
95
- | "playlist-read-private"
96
- | "playlist-read-collaborative"
97
- | "playlist-modify-private"
98
- | "playlist-modify-public"
99
- | "user-follow-modify"
100
- | "user-follow-read"
101
- | "user-top-read"
102
- | "user-read-recently-played"
103
- | "user-library-modify"
104
- | "user-library-read"
105
- | "user-read-email"
106
- | "user-read-private";
309
+ ```
310
+ code=<authorization-code>&redirect_uri=<redirect-uri>&client_id=<client-id>
107
311
  ```
108
312
 
109
- ## Token Swap Example
313
+ Your server POSTs these to `https://accounts.spotify.com/api/token` with `grant_type=authorization_code` and your `CLIENT_SECRET` in the `Authorization` header, then returns Spotify's response verbatim as `application/json`:
110
314
 
111
- An example token swap endpoint has been provided in the `example` project. For it to work it needs your Spotify client details to be included.
315
+ ```json
316
+ {
317
+ "access_token": "BQA...",
318
+ "token_type": "Bearer",
319
+ "expires_in": 3600,
320
+ "refresh_token": "AQA...",
321
+ "scope": "user-read-email streaming"
322
+ }
323
+ ```
324
+
325
+ ### Refresh endpoint (`POST {tokenRefreshURL}`)
112
326
 
113
- 1. Open the `server.js` file and add your client details:
327
+ The native module sends:
114
328
 
115
- ```javascript
116
- const CLIENT_ID = "<your-client-id>";
117
- const CLIENT_SECRET = "<your-client-secret>";
118
329
  ```
330
+ refresh_token=<token>&client_id=<client-id>
331
+ ```
332
+
333
+ Your server POSTs to `https://accounts.spotify.com/api/token` with `grant_type=refresh_token`. Return Spotify's response verbatim. If Spotify rotates the refresh token the response will contain a new `refresh_token`; if not, the field is absent — the library handles both cases correctly.
334
+
335
+ ### Error responses
336
+
337
+ Return a non-2xx HTTP status with a JSON body for structured error propagation. The library will reject with `TOKEN_SWAP_FAILED` and include the status code and (truncated) response body in `e.message`.
119
338
 
120
- These values can be found in your [Spotify Developer Dashboard](https://developer.spotify.com/dashboard). You will need an existing Spotify app for this.
339
+ ### Reference implementation
121
340
 
122
- 2. Run the server
341
+ The example app uses [Expo Router API routes](https://docs.expo.dev/router/reference/api-routes/) for the swap and refresh endpoints — no separate server process needed.
342
+
343
+ **Before running the example, you need a Spotify app:**
344
+
345
+ > **Note (February 2026 onwards):** Spotify now requires the app owner to have an active **Spotify Premium** subscription to use Development Mode apps. Development Mode apps are also limited to **5 test users** — each user must be explicitly added in your Dashboard under **User Management**. See [Spotify's February 2026 migration guide](https://developer.spotify.com/documentation/web-api/tutorials/february-2026-migration-guide) for details.
346
+
347
+ 1. Go to [developer.spotify.com/dashboard](https://developer.spotify.com/dashboard) and create an app (or use an existing one).
348
+ 2. In the app settings, under **APIs used**, enable **Web API** (required for the `/v1/me` profile call).
349
+ 3. Under **Redirect URIs**, add `expo-spotify-sdk-example://authenticate` exactly and save.
350
+ 4. Under **User Management**, add the Spotify accounts that will test the app (up to 5 in Development Mode).
351
+
352
+ **Then populate the credentials:**
123
353
 
124
354
  ```sh
125
- node server.js
355
+ cd example
356
+ cp .env.local.example .env.local
126
357
  ```
127
358
 
128
- 3. Set the `tokenSwapURL` value in your `authenticateAsync` call:
359
+ Edit `.env.local` all three values are required:
129
360
 
130
- ```javascript
131
- const session = await authenticateAsync({
132
- tokenSwapURL: "http://192.168.1.120:3000/swap",
133
- scopes: [
134
- ...
135
- ]
136
- });
361
+ ```sh
362
+ # From your Spotify app's dashboard page
363
+ SPOTIFY_CLIENT_ID=your_client_id_here
364
+ SPOTIFY_CLIENT_SECRET=your_client_secret_here
365
+
366
+ # Must match the redirect URI you registered in the Spotify Dashboard
367
+ SPOTIFY_REDIRECT_URI=expo-spotify-sdk-example://authenticate
137
368
  ```
138
369
 
139
- All authentication requests will now be sent through the token swap server.
370
+ `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` are shown on your app's dashboard page. `SPOTIFY_CLIENT_SECRET` is revealed by clicking **View client secret**.
371
+
372
+ **Then run the app:**
373
+
374
+ ```sh
375
+ npx expo start
376
+ ```
377
+
378
+ The `/swap` and `/refresh` API routes are served by the Expo dev server alongside the app. `Constants.expoConfig.hostUri` auto-detects the correct server URL for any device or simulator — no manual IP configuration needed.
379
+
380
+ ## Troubleshooting
381
+
382
+ **`INVALID_CONFIG: Missing meta-data 'spotifyClientId'`**
383
+ Run `expo prebuild` after adding the plugin to your config. The plugin injects the required `AndroidManifest.xml` entries.
384
+
385
+ **`isAvailable()` returns `false` on Android 11+ release builds**
386
+ Android 11+ requires a `<queries>` element to inspect other apps' package names. The module ships this in its `AndroidManifest.xml`; make sure you are not merging a custom manifest that removes it.
387
+
388
+ **iOS: authentication never returns**
389
+ Ensure your app's URL scheme is registered in Xcode under **Info → URL Types** and that it matches the `scheme` in the plugin config. The `expo prebuild` step does this automatically; if you have a bare workflow, check `CFBundleURLSchemes` in `Info.plist`.
140
390
 
141
- ## Acknowledgments
391
+ **`AUTH_IN_PROGRESS`**
392
+ `authenticateAsync` was called while a previous call was still pending. Wait for the first call to resolve or reject before calling again.
142
393
 
143
- This project has been heavily inspired by the following projects:
394
+ ## Acknowledgements
144
395
 
145
- * [react-native-spotify-remote](https://github.com/cjam/react-native-spotify-remote)
146
- * [expo-spotify](https://github.com/kvbalib/expo-spotify)
396
+ Inspired by [react-native-spotify-remote](https://github.com/cjam/react-native-spotify-remote) and [expo-spotify](https://github.com/kvbalib/expo-spotify).
147
397
 
148
- ## Contribute
398
+ ## Contributing
149
399
 
150
- Contributions are welcome!
400
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
151
401
 
152
402
  ## License
153
403
 
154
- MIT
404
+ MIT — see [LICENSE](LICENSE).
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'expo.modules.spotifysdk'
4
- version = '0.1.1'
4
+ version = '0.5.0'
5
5
 
6
6
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
7
  apply from: expoModulesCorePlugin
@@ -9,25 +9,24 @@ applyKotlinExpoModulesCorePlugin()
9
9
  useCoreDependencies()
10
10
  useExpoPublishing()
11
11
 
12
- // If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
13
- // The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
14
- // Most of the time, you may like to manage the Android SDK versions yourself.
15
- def useManagedAndroidSdkVersions = false
16
- if (useManagedAndroidSdkVersions) {
17
- useDefaultAndroidSdkVersions()
18
- } else {
19
- buildscript {
20
- // Simple helper that allows the root project to override versions declared by this library.
21
- ext.safeExtGet = { prop, fallback ->
22
- rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
23
- }
12
+ buildscript {
13
+ ext.safeExtGet = { prop, fallback ->
14
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
24
15
  }
25
- project.android {
26
- compileSdkVersion safeExtGet("compileSdkVersion", 34)
27
- defaultConfig {
28
- minSdkVersion safeExtGet("minSdkVersion", 21)
29
- targetSdkVersion safeExtGet("targetSdkVersion", 34)
30
- }
16
+ }
17
+
18
+ project.android {
19
+ compileSdkVersion safeExtGet("compileSdkVersion", 35)
20
+ defaultConfig {
21
+ minSdkVersion safeExtGet("minSdkVersion", 24)
22
+ targetSdkVersion safeExtGet("targetSdkVersion", 35)
23
+ }
24
+ compileOptions {
25
+ sourceCompatibility JavaVersion.VERSION_17
26
+ targetCompatibility JavaVersion.VERSION_17
27
+ }
28
+ kotlinOptions {
29
+ jvmTarget = "17"
31
30
  }
32
31
  }
33
32
 
@@ -35,14 +34,21 @@ android {
35
34
  namespace "expo.modules.spotifysdk"
36
35
  defaultConfig {
37
36
  versionCode 1
38
- versionName "0.1.1"
37
+ versionName "0.5.0"
38
+ consumerProguardFiles "consumer-rules.pro"
39
39
  }
40
40
  lintOptions {
41
41
  abortOnError false
42
42
  }
43
43
  }
44
44
 
45
+ repositories {
46
+ mavenCentral()
47
+ }
48
+
45
49
  dependencies {
46
- implementation 'com.squareup.okhttp3:okhttp:4.9.2'
47
- implementation "com.spotify.android:auth:2.1.2"
50
+ implementation "com.spotify.android:auth:4.0.1"
51
+ implementation "com.squareup.okhttp3:okhttp:4.12.0"
52
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1"
53
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"
48
54
  }
@@ -0,0 +1,8 @@
1
+ # Keep Spotify Auth SDK classes
2
+ -keep class com.spotify.sdk.android.auth.** { *; }
3
+ -keepclassmembers class com.spotify.sdk.android.auth.** { *; }
4
+ -dontwarn com.spotify.sdk.android.auth.**
5
+
6
+ # Keep our module's data classes
7
+ -keep class expo.modules.spotifysdk.** { *; }
8
+ -keepclassmembers class expo.modules.spotifysdk.** { *; }
@@ -1,5 +1,8 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <queries>
4
+ <package android:name="com.spotify.music" />
5
+ </queries>
3
6
  <application>
4
7
  <meta-data
5
8
  android:name="spotifyClientId"