@wwdrew/expo-spotify-sdk 2.1.0 → 2.2.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 (86) hide show
  1. package/README.md +32 -2
  2. package/android/build.gradle +4 -5
  3. package/android/libs/SETUP.md +9 -24
  4. package/android/spotify-native-sdk.gradle +118 -0
  5. package/android/src/main/java/expo/modules/spotifysdk/ExpoSpotifySDKModule.kt +12 -1
  6. package/android/src/main/java/expo/modules/spotifysdk/SpotifyAppRemoteCoordinator.kt +32 -0
  7. package/build/app-remote/index.d.ts +34 -0
  8. package/build/app-remote/index.d.ts.map +1 -1
  9. package/build/app-remote/index.js +36 -0
  10. package/build/app-remote/index.js.map +1 -1
  11. package/ios/ExpoSpotifyAppDelegate.swift +8 -1
  12. package/ios/ExpoSpotifySDK.podspec +26 -2
  13. package/ios/ExpoSpotifySDKModule.swift +16 -1
  14. package/ios/SpotifyAppRemoteCoordinator.swift +108 -0
  15. package/ios/spotify-native-sdk-versions.json +12 -0
  16. package/package.json +10 -4
  17. package/ios/SpotifySDK/Licenses/MPMessagePack-LICENSE.md +0 -20
  18. package/ios/SpotifySDK/SpotifyiOS.xcframework/Info.plist +0 -40
  19. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemote.h +0 -287
  20. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteAlbum.h +0 -18
  21. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteArtist.h +0 -18
  22. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteCommon.h +0 -37
  23. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectionParams.h +0 -65
  24. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityAPI.h +0 -55
  25. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityState.h +0 -15
  26. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteContentAPI.h +0 -78
  27. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteContentItem.h +0 -49
  28. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteCrossfadeState.h +0 -18
  29. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteImageAPI.h +0 -25
  30. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteImageRepresentable.h +0 -16
  31. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteLibraryState.h +0 -19
  32. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackOptions.h +0 -32
  33. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackRestrictions.h +0 -32
  34. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlayerAPI.h +0 -267
  35. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePlayerState.h +0 -40
  36. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemotePodcastPlaybackSpeed.h +0 -15
  37. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteTrack.h +0 -52
  38. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteUserAPI.h +0 -102
  39. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTAppRemoteUserCapabilities.h +0 -15
  40. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTConfiguration.h +0 -66
  41. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTError.h +0 -31
  42. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTLogin.h +0 -10
  43. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTMacros.h +0 -125
  44. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTScope.h +0 -75
  45. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTSession.h +0 -33
  46. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SPTSessionManager.h +0 -170
  47. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SpotifyAppRemote.h +0 -28
  48. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Headers/SpotifyiOS.h +0 -7
  49. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Info.plist +0 -0
  50. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/Modules/module.modulemap +0 -5
  51. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/PrivacyInfo.xcprivacy +0 -14
  52. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64/SpotifyiOS.framework/SpotifyiOS +0 -0
  53. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemote.h +0 -287
  54. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteAlbum.h +0 -18
  55. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteArtist.h +0 -18
  56. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteCommon.h +0 -37
  57. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectionParams.h +0 -65
  58. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityAPI.h +0 -55
  59. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteConnectivityState.h +0 -15
  60. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteContentAPI.h +0 -78
  61. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteContentItem.h +0 -49
  62. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteCrossfadeState.h +0 -18
  63. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteImageAPI.h +0 -25
  64. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteImageRepresentable.h +0 -16
  65. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteLibraryState.h +0 -19
  66. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackOptions.h +0 -32
  67. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlaybackRestrictions.h +0 -32
  68. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlayerAPI.h +0 -267
  69. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePlayerState.h +0 -40
  70. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemotePodcastPlaybackSpeed.h +0 -15
  71. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteTrack.h +0 -52
  72. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteUserAPI.h +0 -102
  73. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTAppRemoteUserCapabilities.h +0 -15
  74. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTConfiguration.h +0 -66
  75. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTError.h +0 -31
  76. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTLogin.h +0 -10
  77. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTMacros.h +0 -125
  78. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTScope.h +0 -75
  79. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTSession.h +0 -33
  80. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SPTSessionManager.h +0 -170
  81. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SpotifyAppRemote.h +0 -28
  82. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Headers/SpotifyiOS.h +0 -7
  83. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Info.plist +0 -0
  84. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/Modules/module.modulemap +0 -5
  85. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/PrivacyInfo.xcprivacy +0 -14
  86. package/ios/SpotifySDK/SpotifyiOS.xcframework/ios-arm64_x86_64-simulator/SpotifyiOS.framework/SpotifyiOS +0 -0
package/README.md CHANGED
@@ -90,6 +90,7 @@ Top-level v0-style functions were removed in **`2.x`**. If you still use them, s
90
90
  npx expo install @wwdrew/expo-spotify-sdk
91
91
 
92
92
  # Expo SDK 55 only: npx expo install @wwdrew/expo-spotify-sdk@1
93
+ # (or @sdk55 — npm dist-tag for the v1 maintenance lane)
93
94
 
94
95
  # 2. Add the config plugin to app.config.ts / app.json (see Configuration below)
95
96
  # 3. Regenerate native projects
@@ -98,6 +99,20 @@ npx expo prebuild
98
99
 
99
100
  For bare React Native (no Expo CLI), see [Installation in bare React Native](#installation-in-bare-react-native).
100
101
 
102
+ ## Native SDK binaries
103
+
104
+ | Platform | Distribution |
105
+ | --- | --- |
106
+ | **iOS** | `SpotifyiOS` resolved from Spotify's GitHub via SPM at `pod install` ([ADR-0008](./docs/adr/0008-ios-spotify-sdk-via-spm.md)). Uses React Native's `spm_dependency` (RN 0.75+). Supported Expo SDK lanes: [Platform support](#platform-support). |
107
+ | **Android** | App Remote AAR downloaded from Spotify's GitHub at Gradle build. Auth SDK from Maven Central. |
108
+
109
+ | You are… | Action |
110
+ | --- | --- |
111
+ | **Using the published npm package** | Network on first native build per platform (`pod install` / Gradle). |
112
+ | **Developing this repo from git** | Same — no manual fetch scripts. |
113
+
114
+ See [Native SDK distribution](./docs/guides/native-sdk-distribution.md).
115
+
101
116
  ## Installation in bare React Native
102
117
 
103
118
  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.
@@ -340,7 +355,20 @@ export function useAutoConnectAppRemote(options?: { once?: boolean }) {
340
355
  }
341
356
  ```
342
357
 
343
- For iOS, if `connect()` fails with `CONNECTION_FAILED`, foreground the Spotify app and retry manually see [Troubleshooting](#troubleshooting).
358
+ For iOS, if `connect()` fails with `CONNECTION_FAILED`, the Spotify app has likely been suspended in the background — `connect()` can only attach to an already-running Spotify. Instead of asking the user to foreground Spotify and retry manually, call `AppRemote.authorizeAndPlay(accessToken, uri?)`, which wakes Spotify (launching it if needed), starts playback, and then connects:
359
+
360
+ ```ts
361
+ try {
362
+ await AppRemote.connect(session.accessToken);
363
+ } catch (e) {
364
+ if (e instanceof AppRemoteError && e.code === "CONNECTION_FAILED") {
365
+ // Spotify was suspended — wake it, start playing, and connect.
366
+ await AppRemote.authorizeAndPlay(session.accessToken);
367
+ }
368
+ }
369
+ ```
370
+
371
+ Note that `authorizeAndPlay()` always starts (or resumes) playback and briefly switches to the Spotify app — see [Troubleshooting](#troubleshooting).
344
372
 
345
373
  ## Spotify Premium and App Remote
346
374
 
@@ -379,6 +407,7 @@ Need the deprecated shims temporarily? Pin **`1.x`**: `npm install @wwdrew/expo-
379
407
  | [Error codes](./docs/error-codes.md) | Per-namespace codes with when/what-to-do |
380
408
  | [App Remote error mapping](./docs/app-remote-error-mapping.md) | iOS/Android native → JS mapping matrix |
381
409
  | [Platform differences](./docs/guides/platform-differences.md) | iOS vs Android parity |
410
+ | [Native SDK distribution](./docs/guides/native-sdk-distribution.md) | How iOS/Android binaries are fetched, packaged, and bumped |
382
411
  | [Token swap server](./docs/guides/token-swap-server.md) | Swap/refresh endpoints + example app setup |
383
412
 
384
413
  ## Troubleshooting
@@ -400,7 +429,7 @@ On iOS this can also be a stuck state when Spotify never redirects back. Call `A
400
429
  **App Remote: `CONNECTION_FAILED` / `Connection refused` (iOS code 61)**
401
430
  The Spotify app is installed but its App Remote transport is not accepting connections. Common causes:
402
431
 
403
- 1. **Spotify is not in the foreground** — switch to Spotify, then retry `AppRemote.connect()`.
432
+ 1. **Spotify is suspended / not in the foreground** — `connect()` only attaches to a running Spotify. Call `AppRemote.authorizeAndPlay(accessToken, uri?)` to wake it (it launches Spotify, starts playback, and connects), or have the user switch to Spotify and retry `AppRemote.connect()`.
404
433
  2. **Connect ran before auth finished** — call `AppRemote.connect()` only after `Auth.authenticate()` resolves.
405
434
  3. **Stale access token** — refresh or re-authenticate, then reconnect.
406
435
  4. **Retry loop on startup** — avoid calling `connect()` on every render while `connectionState === "disconnected"`; gate on a one-shot flag or user action.
@@ -414,6 +443,7 @@ App Remote player state is limited for non-Premium users. Check `GET /v1/me` →
414
443
  ## Related docs
415
444
 
416
445
  - [CONTEXT.md](./CONTEXT.md) — terminology (Auth SDK vs App Remote vs Web API)
446
+ - [docs/guides/native-sdk-distribution.md](./docs/guides/native-sdk-distribution.md) — how native binaries are fetched and packaged
417
447
  - [docs/api-reference.md](./docs/api-reference.md) — method reference
418
448
  - [docs/error-codes.md](./docs/error-codes.md) — error code tables
419
449
  - [docs/QA_CHECKLIST.md](./docs/QA_CHECKLIST.md) — manual QA before a `2.x` release on `main` (or `1.x` on `v1`)
@@ -1,4 +1,5 @@
1
1
  apply plugin: 'com.android.library'
2
+ apply from: 'spotify-native-sdk.gradle'
2
3
 
3
4
  group = 'expo.modules.spotifysdk'
4
5
  version = '0.5.0'
@@ -49,11 +50,9 @@ repositories {
49
50
  dependencies {
50
51
  implementation "com.spotify.android:auth:4.0.1"
51
52
 
52
- // Spotify App Remote SDK — NOT on Maven Central.
53
- // Download spotify-app-remote-release-0.8.0.aar from
54
- // https://github.com/spotify/android-sdk/releases and place it in android/libs/.
55
- // See android/libs/SETUP.md for full instructions.
56
- implementation files('libs/spotify-app-remote-release-0.8.0.aar')
53
+ // Spotify App Remote SDK — NOT on Maven Central; downloaded at build time
54
+ // (see android/spotify-native-sdk.gradle and ios/spotify-native-sdk-versions.json).
55
+ implementation files(spotifyAppRemoteAarFile)
57
56
  // App Remote is consumed as a local AAR, so transitive deps like Gson must
58
57
  // be declared explicitly to avoid NoClassDefFoundError at runtime.
59
58
  implementation "com.google.code.gson:gson:2.11.0"
@@ -1,29 +1,14 @@
1
- # Android App Remote SDK setup
1
+ # Android App Remote SDK
2
2
 
3
- The Spotify App Remote SDK for Android is not published to Maven Central and must be
4
- downloaded manually.
3
+ The Spotify App Remote SDK for Android is **not** on Maven Central. This module
4
+ downloads `spotify-app-remote-release-<version>.aar` from Spotify's GitHub
5
+ releases at Gradle build time (`android/spotify-native-sdk.gradle`).
5
6
 
6
- ## Steps
7
+ Version pin: `ios/spotify-native-sdk-versions.json` (`android` section).
7
8
 
8
- 1. Go to <https://github.com/spotify/android-sdk/releases>
9
- 2. Download the latest release archive (e.g. `spotify-android-sdk-x.x.x.zip`)
10
- 3. Extract `spotify-app-remote-release-x.x.x.aar` from the archive
11
- 4. Rename it to `spotify-app-remote-release-0.8.0.aar` (or update the version in
12
- `android/build.gradle` to match the downloaded version)
13
- 5. Place the `.aar` file in this directory (`android/libs/`)
9
+ The downloaded `.aar` is gitignored under `android/libs/`.
14
10
 
15
- The `flatDir` repository in `android/build.gradle` will pick it up automatically.
11
+ ## Bumping the App Remote version
16
12
 
17
- ## Why not Maven?
18
-
19
- The Spotify Auth SDK (`com.spotify.android:auth`) is on Maven Central, but the App Remote
20
- SDK is distributed exclusively via GitHub releases. This directory follows the same pattern
21
- as the iOS side, where `SpotifyiOS.xcframework` is committed directly to `ios/SpotifySDK/`.
22
-
23
- ## Proguard
24
-
25
- `consumer-rules.pro` already keeps the App Remote classes:
26
-
27
- ```
28
- -keep class com.spotify.android.appremote.** { *; }
29
- ```
13
+ Update `appRemoteVersion`, `appRemoteReleaseTag`, and `appRemoteSha256` in
14
+ `ios/spotify-native-sdk-versions.json`, then ship a new release of this package.
@@ -0,0 +1,118 @@
1
+ // Downloads Spotify App Remote AAR at Gradle build time (pinned in ios/spotify-native-sdk-versions.json).
2
+ def spotifyNativeVersionsFile = file("${projectDir}/../ios/spotify-native-sdk-versions.json")
3
+ if (!spotifyNativeVersionsFile.exists()) {
4
+ throw new GradleException(
5
+ "Missing ${spotifyNativeVersionsFile}. Native SDK versions must be pinned in ios/spotify-native-sdk-versions.json."
6
+ )
7
+ }
8
+
9
+ def spotifyNative = new groovy.json.JsonSlurper().parse(spotifyNativeVersionsFile)
10
+ def androidSdk = spotifyNative.android
11
+ if (androidSdk == null) {
12
+ throw new GradleException("ios/spotify-native-sdk-versions.json is missing an \"android\" section.")
13
+ }
14
+
15
+ def appRemoteVersion = androidSdk.appRemoteVersion as String
16
+ def appRemoteReleaseTag = androidSdk.appRemoteReleaseTag as String
17
+ def appRemoteSha256 = androidSdk.appRemoteSha256 as String
18
+
19
+ if (!appRemoteVersion || !appRemoteReleaseTag || !appRemoteSha256) {
20
+ throw new GradleException(
21
+ "ios/spotify-native-sdk-versions.json android section is missing required fields: " +
22
+ "appRemoteVersion, appRemoteReleaseTag, appRemoteSha256"
23
+ )
24
+ }
25
+
26
+ def libsDir = file("${projectDir}/libs")
27
+ def aarFile = file("${libsDir}/spotify-app-remote-release-${appRemoteVersion}.aar")
28
+ def versionMarker = file("${libsDir}/.version")
29
+ def downloadUrl =
30
+ "https://github.com/spotify/android-sdk/releases/download/${appRemoteReleaseTag}/spotify-app-remote-release-${appRemoteVersion}.aar"
31
+
32
+ def sha256File = { File f ->
33
+ def digest = java.security.MessageDigest.getInstance("SHA-256")
34
+ f.withInputStream { stream ->
35
+ def buffer = new byte[8192]
36
+ int read
37
+ while ((read = stream.read(buffer)) > 0) {
38
+ digest.update(buffer, 0, read)
39
+ }
40
+ }
41
+ digest.digest().collect { String.format("%02x", it) }.join("")
42
+ }
43
+
44
+ def appRemoteAarUpToDate = {
45
+ if (!aarFile.exists()) {
46
+ return false
47
+ }
48
+ if (!versionMarker.exists() || versionMarker.text.trim() != appRemoteVersion) {
49
+ return false
50
+ }
51
+ sha256File(aarFile) == appRemoteSha256
52
+ }
53
+
54
+ tasks.register("downloadSpotifyAppRemoteAar") {
55
+ group = "spotify"
56
+ description = "Download Spotify App Remote AAR from GitHub releases"
57
+
58
+ outputs.file(aarFile)
59
+ outputs.file(versionMarker)
60
+
61
+ onlyIf {
62
+ !appRemoteAarUpToDate()
63
+ }
64
+
65
+ doLast {
66
+ libsDir.mkdirs()
67
+ logger.lifecycle("ExpoSpotifySDK: downloading Spotify App Remote ${appRemoteVersion}")
68
+
69
+ def tmp = File.createTempFile("spotify-app-remote", ".aar")
70
+ try {
71
+ ant.get(
72
+ src: downloadUrl,
73
+ dest: tmp,
74
+ verbose: true,
75
+ connecttimeout: 30000,
76
+ readtimeout: 60000
77
+ )
78
+ def actual = sha256File(tmp)
79
+ if (actual != appRemoteSha256) {
80
+ throw new GradleException(
81
+ "Spotify App Remote AAR checksum mismatch\n" +
82
+ " expected ${appRemoteSha256}\n" +
83
+ " actual ${actual}"
84
+ )
85
+ }
86
+
87
+ libsDir.listFiles()?.each { f ->
88
+ if (f.name.endsWith(".aar") && f.name != aarFile.name) {
89
+ f.delete()
90
+ }
91
+ }
92
+
93
+ if (aarFile.exists()) {
94
+ aarFile.delete()
95
+ }
96
+
97
+ if (!tmp.renameTo(aarFile)) {
98
+ tmp.withInputStream { input ->
99
+ aarFile.withOutputStream { output ->
100
+ output << input
101
+ }
102
+ }
103
+ }
104
+
105
+ versionMarker.text = appRemoteVersion
106
+ } finally {
107
+ tmp.delete()
108
+ }
109
+
110
+ logger.lifecycle("ExpoSpotifySDK: Spotify App Remote AAR ready at ${aarFile}")
111
+ }
112
+ }
113
+
114
+ ext.spotifyAppRemoteAarFile = aarFile
115
+
116
+ tasks.named("preBuild").configure {
117
+ dependsOn("downloadSpotifyAppRemoteAar")
118
+ }
@@ -8,7 +8,7 @@ import expo.modules.kotlin.functions.Coroutine
8
8
  import expo.modules.kotlin.modules.Module
9
9
  import expo.modules.kotlin.modules.ModuleDefinition
10
10
 
11
- private const val SDK_VERSION = "2.1.0" // x-release-please-version
11
+ private const val SDK_VERSION = "2.2.0" // x-release-please-version
12
12
  private const val EVENT_SESSION_CHANGE = "onSessionChange"
13
13
  private const val EVENT_CONNECTION_STATE_CHANGE = "onConnectionStateChange"
14
14
  private const val EVENT_CONNECTION_ERROR = "onConnectionError"
@@ -202,6 +202,17 @@ class ExpoSpotifySDKModule : Module() {
202
202
  )
203
203
  }
204
204
 
205
+ AsyncFunction("appRemoteAuthorizeAndPlay") Coroutine { accessToken: String, uri: String ->
206
+ val manifest = readManifestConfig()
207
+ appRemoteCoordinator.authorizeAndPlay(
208
+ context = context,
209
+ clientId = manifest.clientId,
210
+ redirectUri = manifest.redirectUri,
211
+ accessToken = accessToken,
212
+ uri = uri,
213
+ )
214
+ }
215
+
205
216
  AsyncFunction("appRemoteDisconnect") Coroutine { ->
206
217
  appRemoteCoordinator.disconnect()
207
218
  }
@@ -131,6 +131,38 @@ class SpotifyAppRemoteCoordinator {
131
131
  }
132
132
  }
133
133
 
134
+ /**
135
+ * Wakes the Spotify app and starts playback, then ensures an active App
136
+ * Remote connection — the cross-platform counterpart of iOS
137
+ * `authorizeAndPlayURI`.
138
+ *
139
+ * On Android there is no dedicated "authorize and play" entry point:
140
+ * [SpotifyAppRemote.connect] already launches/wakes the Spotify service when
141
+ * it is installed, so this simply connects (if not already connected) and
142
+ * then issues a play (or resume, for an empty [uri]) command. The resulting
143
+ * playback keeps Spotify alive, mirroring the iOS behaviour.
144
+ *
145
+ * @param uri Spotify URI to play, or empty to resume the last/contextual track.
146
+ */
147
+ suspend fun authorizeAndPlay(
148
+ context: Context,
149
+ clientId: String,
150
+ redirectUri: String,
151
+ accessToken: String,
152
+ uri: String,
153
+ ) {
154
+ if (appRemote?.isConnected != true) {
155
+ connect(context, clientId, redirectUri, accessToken)
156
+ }
157
+ if (uri.isEmpty()) {
158
+ requireConnected("AppRemote.authorizeAndPlay").playerApi.resume()
159
+ .awaitVoid("AppRemote.authorizeAndPlay")
160
+ } else {
161
+ requireConnected("AppRemote.authorizeAndPlay").playerApi.play(uri)
162
+ .awaitVoid("AppRemote.authorizeAndPlay")
163
+ }
164
+ }
165
+
134
166
  /**
135
167
  * Disconnects from the Spotify app. Safe to call when already disconnected.
136
168
  */
@@ -48,6 +48,40 @@ export declare const AppRemote: {
48
48
  * Calling `connect()` while already connected is a no-op.
49
49
  */
50
50
  readonly connect: (accessToken: string) => Promise<void>;
51
+ /**
52
+ * Wakes the Spotify app — launching it if it has been suspended in the
53
+ * background — starts playback, and then establishes the App Remote
54
+ * connection. This is the recommended way to (re)connect when Spotify may
55
+ * not currently be running.
56
+ *
57
+ * Why this exists: {@link connect} only attaches to an *already-running*
58
+ * Spotify process. If Spotify has been suspended (e.g. iOS reclaims a
59
+ * backgrounded app that isn't playing audio, or the connection dropped
60
+ * ~30s after the user paused), `connect()` rejects with `CONNECTION_FAILED`
61
+ * and there is no way to revive Spotify from `connect()` alone.
62
+ * `authorizeAndPlay()` performs an app-switch to Spotify
63
+ * (iOS `authorizeAndPlayURI`; Android wakes the App Remote service and
64
+ * issues a play command), which both revives Spotify and keeps it alive by
65
+ * starting audio.
66
+ *
67
+ * Trade-offs:
68
+ * - It **always starts (or resumes) playback** — it cannot wake Spotify
69
+ * silently.
70
+ * - It briefly **switches to the Spotify app** and back; the OS does not
71
+ * allow waking another app without a foreground app-switch.
72
+ * - Requires Spotify Premium for on-demand playback of a specific `uri`.
73
+ *
74
+ * @param accessToken Access token from `Auth.authenticate()`. On Android this
75
+ * is an iOS-parity parameter (see {@link connect}); on iOS the token
76
+ * returned by the authorization redirect is used to complete the connection.
77
+ * @param uri Spotify URI to play. Pass an empty string (the default) to
78
+ * resume the user's last track or play a contextual/random track.
79
+ *
80
+ * Resolves once Spotify is connected and the play command has been issued;
81
+ * rejects with an {@link AppRemoteError} if Spotify is not installed or the
82
+ * authorization is denied.
83
+ */
84
+ readonly authorizeAndPlay: (accessToken: string, uri?: string) => Promise<void>;
51
85
  /**
52
86
  * Disconnects from the Spotify app. Safe to call when already disconnected.
53
87
  * Resolves once the disconnection is complete.
@@ -1 +1 @@
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
+ {"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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;6CAC2B,MAAM,QAAO,MAAM,KAAQ,OAAO,CAAC,IAAI,CAAC;IAOtE;;;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"}
@@ -55,6 +55,42 @@ export const AppRemote = {
55
55
  connect(accessToken) {
56
56
  return ExpoSpotifySDKModule.appRemoteConnect(accessToken).catch(rethrowAsAppRemoteError);
57
57
  },
58
+ /**
59
+ * Wakes the Spotify app — launching it if it has been suspended in the
60
+ * background — starts playback, and then establishes the App Remote
61
+ * connection. This is the recommended way to (re)connect when Spotify may
62
+ * not currently be running.
63
+ *
64
+ * Why this exists: {@link connect} only attaches to an *already-running*
65
+ * Spotify process. If Spotify has been suspended (e.g. iOS reclaims a
66
+ * backgrounded app that isn't playing audio, or the connection dropped
67
+ * ~30s after the user paused), `connect()` rejects with `CONNECTION_FAILED`
68
+ * and there is no way to revive Spotify from `connect()` alone.
69
+ * `authorizeAndPlay()` performs an app-switch to Spotify
70
+ * (iOS `authorizeAndPlayURI`; Android wakes the App Remote service and
71
+ * issues a play command), which both revives Spotify and keeps it alive by
72
+ * starting audio.
73
+ *
74
+ * Trade-offs:
75
+ * - It **always starts (or resumes) playback** — it cannot wake Spotify
76
+ * silently.
77
+ * - It briefly **switches to the Spotify app** and back; the OS does not
78
+ * allow waking another app without a foreground app-switch.
79
+ * - Requires Spotify Premium for on-demand playback of a specific `uri`.
80
+ *
81
+ * @param accessToken Access token from `Auth.authenticate()`. On Android this
82
+ * is an iOS-parity parameter (see {@link connect}); on iOS the token
83
+ * returned by the authorization redirect is used to complete the connection.
84
+ * @param uri Spotify URI to play. Pass an empty string (the default) to
85
+ * resume the user's last track or play a contextual/random track.
86
+ *
87
+ * Resolves once Spotify is connected and the play command has been issued;
88
+ * rejects with an {@link AppRemoteError} if Spotify is not installed or the
89
+ * authorization is denied.
90
+ */
91
+ authorizeAndPlay(accessToken, uri = "") {
92
+ return ExpoSpotifySDKModule.appRemoteAuthorizeAndPlay(accessToken, uri).catch(rethrowAsAppRemoteError);
93
+ },
58
94
  /**
59
95
  * Disconnects from the Spotify app. Safe to call when already disconnected.
60
96
  * Resolves once the disconnection is complete.
@@ -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,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"]}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,gBAAgB,CAAC,WAAmB,EAAE,MAAc,EAAE;QACpD,OAAO,oBAAoB,CAAC,yBAAyB,CACnD,WAAW,EACX,GAAG,CACJ,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;IACnC,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 * Wakes the Spotify app — launching it if it has been suspended in the\n * background — starts playback, and then establishes the App Remote\n * connection. This is the recommended way to (re)connect when Spotify may\n * not currently be running.\n *\n * Why this exists: {@link connect} only attaches to an *already-running*\n * Spotify process. If Spotify has been suspended (e.g. iOS reclaims a\n * backgrounded app that isn't playing audio, or the connection dropped\n * ~30s after the user paused), `connect()` rejects with `CONNECTION_FAILED`\n * and there is no way to revive Spotify from `connect()` alone.\n * `authorizeAndPlay()` performs an app-switch to Spotify\n * (iOS `authorizeAndPlayURI`; Android wakes the App Remote service and\n * issues a play command), which both revives Spotify and keeps it alive by\n * starting audio.\n *\n * Trade-offs:\n * - It **always starts (or resumes) playback** — it cannot wake Spotify\n * silently.\n * - It briefly **switches to the Spotify app** and back; the OS does not\n * allow waking another app without a foreground app-switch.\n * - Requires Spotify Premium for on-demand playback of a specific `uri`.\n *\n * @param accessToken Access token from `Auth.authenticate()`. On Android this\n * is an iOS-parity parameter (see {@link connect}); on iOS the token\n * returned by the authorization redirect is used to complete the connection.\n * @param uri Spotify URI to play. Pass an empty string (the default) to\n * resume the user's last track or play a contextual/random track.\n *\n * Resolves once Spotify is connected and the play command has been issued;\n * rejects with an {@link AppRemoteError} if Spotify is not installed or the\n * authorization is denied.\n */\n authorizeAndPlay(accessToken: string, uri: string = \"\"): Promise<void> {\n return ExpoSpotifySDKModule.appRemoteAuthorizeAndPlay(\n accessToken,\n uri,\n ).catch(rethrowAsAppRemoteError);\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"]}
@@ -1,7 +1,7 @@
1
1
  import ExpoModulesCore
2
2
  import SpotifyiOS
3
3
 
4
- /// Delivers Spotify auth redirect URLs received via the legacy
4
+ /// Delivers Spotify redirect URLs received via the legacy
5
5
  /// `application(_:open:url:)` AppDelegate path.
6
6
  public class ExpoSpotifyAppDelegate: ExpoAppDelegateSubscriber {
7
7
  public func application(
@@ -9,6 +9,13 @@ public class ExpoSpotifyAppDelegate: ExpoAppDelegateSubscriber {
9
9
  open url: URL,
10
10
  options: [UIApplication.OpenURLOptionsKey: Any] = [:]
11
11
  ) -> Bool {
12
+ // Both the auth flow (SPTSessionManager) and App Remote's
13
+ // `authorizeAndPlay()` redirect through the same URL scheme. Offer the URL
14
+ // to the App Remote coordinator first — it only claims the redirect when an
15
+ // `authorizeAndPlay()` flow is actually pending — then fall back to auth.
16
+ if SpotifyAppRemoteCoordinator.shared?.handleAuthorizeRedirect(url) == true {
17
+ return true
18
+ }
12
19
  return SpotifyAuthCoordinator.shared?.handleOpenURL(url, options: options) ?? false
13
20
  }
14
21
  }
@@ -1,6 +1,18 @@
1
1
  require 'json'
2
2
 
3
+ absolute_react_native_path = ''
4
+ if !ENV['REACT_NATIVE_PATH'].nil?
5
+ absolute_react_native_path = File.expand_path(ENV['REACT_NATIVE_PATH'], Pod::Config.instance.project_root)
6
+ else
7
+ absolute_react_native_path = File.dirname(`node --print "require.resolve('react-native/package.json')"`)
8
+ end
9
+
10
+ unless defined?(spm_dependency)
11
+ require File.join(absolute_react_native_path, "scripts/react_native_pods")
12
+ end
13
+
3
14
  package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
15
+ spotify_native = JSON.parse(File.read(File.join(__dir__, 'spotify-native-sdk-versions.json')))
4
16
 
5
17
  Pod::Spec.new do |s|
6
18
  s.name = 'ExpoSpotifySDK'
@@ -23,6 +35,18 @@ Pod::Spec.new do |s|
23
35
  }
24
36
 
25
37
  s.source_files = "**/*.{h,m,swift}"
26
- s.exclude_files = "SpotifySDK/SpotifyiOS.xcframework/**/*.h"
27
- s.vendored_frameworks = "SpotifySDK/SpotifyiOS.xcframework"
38
+
39
+ ios_sdk = spotify_native['ios']
40
+ if ios_sdk.nil? ||
41
+ ios_sdk['spmRepositoryUrl'].to_s.empty? ||
42
+ ios_sdk['spmVersion'].to_s.empty? ||
43
+ ios_sdk['spmProduct'].to_s.empty?
44
+ raise "ios/spotify-native-sdk-versions.json is missing required iOS fields: spmRepositoryUrl, spmVersion, spmProduct"
45
+ end
46
+
47
+ spm_dependency(s,
48
+ url: ios_sdk['spmRepositoryUrl'],
49
+ requirement: { kind: 'exactVersion', version: ios_sdk['spmVersion'] },
50
+ products: [ios_sdk['spmProduct']]
51
+ )
28
52
  end
@@ -1,7 +1,7 @@
1
1
  import ExpoModulesCore
2
2
  import SpotifyiOS
3
3
 
4
- private let SDK_VERSION = "2.1.0" // x-release-please-version
4
+ private let SDK_VERSION = "2.2.0" // x-release-please-version
5
5
  private let EVENT_SESSION_CHANGE = "onSessionChange"
6
6
  private let EVENT_CONNECTION_STATE_CHANGE = "onConnectionStateChange"
7
7
  private let EVENT_CONNECTION_ERROR = "onConnectionError"
@@ -138,6 +138,21 @@ public class ExpoSpotifySDKModule: Module {
138
138
  }
139
139
  }
140
140
 
141
+ AsyncFunction("appRemoteAuthorizeAndPlay") { (accessToken: String, uri: String) async throws -> Void in
142
+ guard let coordinator = SpotifyAppRemoteCoordinator.shared else {
143
+ throw AppRemoteException(AppRemoteError.connectionFailed(
144
+ "Missing `ExpoSpotifySDK` configuration in Info.plist."
145
+ ))
146
+ }
147
+ do {
148
+ try await coordinator.authorizeAndPlay(uri: uri, accessToken: accessToken)
149
+ } catch let error as AppRemoteError {
150
+ throw AppRemoteException(error)
151
+ } catch let error as NativePlayerError {
152
+ throw PlayerException(error)
153
+ }
154
+ }
155
+
141
156
  AsyncFunction("appRemoteDisconnect") { () async -> Void in
142
157
  await SpotifyAppRemoteCoordinator.shared?.disconnect()
143
158
  }