@wwdrew/expo-spotify-sdk
v2.1.0
Published
Expo module wrapping the native Spotify iOS (v5) and Android (v4) SDKs for OAuth authentication and App Remote playback control
Downloads
1,518
Maintainers
Readme
expo-spotify-sdk
An Expo module that wraps the native Spotify iOS SDK (v5.0.1) and Spotify Android SDK (v4.0.1) to provide Spotify Auth + App Remote control in Expo and React Native apps.
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.
Table of contents
- Platform support
- Versioning and Expo SDK lanes
- Public API (Auth + App Remote)
- Quick start (Expo)
- Installation in bare React Native
- Configuration
- Usage
- Spotify Premium and App Remote
- Migration from v0.x
- Documentation
- Troubleshooting
- Related docs
- Acknowledgements
- Contributing
- License
Platform support
iOS and Android only. This module will not support Expo Web or any browser target — there is no web implementation and none is planned.
| Feature | iOS | Android |
| ------------- | --- | ------- |
| Auth.* | ✅ | ✅ |
| AppRemote.* | ✅ | ✅ |
| Player.* | ✅ | ✅ |
| User.* | ✅ | ✅ |
| Content.* | ✅ | ✅ |
| Images.* | ✅ | ✅ |
Versioning and Expo SDK lanes
Install the major that matches your Expo SDK:
| npm version | Expo SDK | iOS minimum | Branch |
| ----------- | -------- | ----------- | ----------------------------- |
| 1.x | 55 | 15.1 | v1 (long-lived maintenance) |
| 2.x | 56+ | 16.4 | main |
Both lanes ship the same public API (Auth + App Remote namespaces and hooks). The major version signals runtime lane, not a different feature set. See ADR-0005.
The current main branch targets Expo SDK 56 and releases as 2.x. For Expo SDK 55, install 1.x from the v1 branch (ADR-0005).
Auth payload note by lane:
2.x(main): Android token swap/refresh requests are normalized to match iOS (codefor swap,refresh_tokenfor refresh).1.x(v1): Android keeps the legacy payload shape that includes additional form fields for compatibility with existing backends.
Public API (Auth + App Remote)
import {
Auth,
AppRemote,
Player,
User,
Content,
Images,
SpotifyURI,
useSession,
useConnectionState,
usePlayerState,
useCurrentTrack,
useIsPlaying,
usePlaybackPosition,
useCapabilities,
useLibraryState,
} from "@wwdrew/expo-spotify-sdk";Top-level v0-style functions were removed in 2.x. If you still use them, stay on 1.x or migrate — see Migration from v0.x.
Not wrapped: Spotify Web API (api.spotify.com). Use the access token from Auth.authenticate() and call REST yourself. See CONTEXT.md for terminology.
Quick start (Expo)
# 1. Install (Expo SDK 56+ — matches `main` / npm `2.x`)
npx expo install @wwdrew/expo-spotify-sdk
# Expo SDK 55 only: npx expo install @wwdrew/expo-spotify-sdk@1
# 2. Add the config plugin to app.config.ts / app.json (see Configuration below)
# 3. Regenerate native projects
npx expo prebuildFor bare React Native (no Expo CLI), see Installation in bare React Native.
Installation in bare React Native
This library is an Expo Module 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.
1. Install
npm install @wwdrew/expo-spotify-sdk expo-modules-core
# or
yarn add @wwdrew/expo-spotify-sdk expo-modules-core2. iOS
Add the pod to your Podfile:
pod 'ExpoSpotifySDK', :path => '../node_modules/@wwdrew/expo-spotify-sdk'Then install pods:
cd ios && pod installIf you have not already bootstrapped expo-modules-core in your AppDelegate, follow the Expo Modules integration guide first — in particular, your AppDelegate must inherit from ExpoAppDelegate (or call ExpoModulesAppDelegateSubscriber) so that the Spotify redirect URL is handled correctly.
Finally, register your URL scheme. In Xcode open Info → URL Types and add a new entry with:
- Identifier:
$(PRODUCT_BUNDLE_IDENTIFIER) - URL Schemes: the value you'll pass as
schemein the plugin config (e.g.myapp)
3. Android
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.
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:
android {
defaultConfig {
// ...
manifestPlaceholders = [
spotifyClientId: "your-spotify-client-id",
spotifyRedirectUri: "myapp://spotify-auth",
redirectSchemeName: "myapp",
redirectHostName: "spotify-auth",
redirectPathPattern: ".*"
]
}
}
dependencies {
// ...
implementation 'com.spotify.android:auth:4.0.1'
implementation 'com.squareup.okhttp3:okhttp:4.12.0' // required for token swap/refresh
}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.
Configuration
Add the plugin to your app.config.ts (or app.json).
Typed plugin (Expo SDK 56+)
Import from @wwdrew/expo-spotify-sdk/plugin for autocomplete and type-checked options:
import type { ExpoConfig } from "expo/config";
import withSpotifySdk from "@wwdrew/expo-spotify-sdk/plugin";
export default ({ config }: { config: ExpoConfig }): ExpoConfig => ({
...config,
plugins: [
withSpotifySdk({
clientID: "your-spotify-client-id",
scheme: "myapp",
host: "spotify-auth",
}),
],
});String tuple (all SDK versions)
export default {
plugins: [
[
"@wwdrew/expo-spotify-sdk",
{
clientID: "your-spotify-client-id",
scheme: "myapp",
host: "spotify-auth",
},
],
],
};redirectPathPattern is optional and defaults to ".*", which matches every redirect URI shape Spotify will hand back. Only set it if you have a specific path registered in your Spotify app settings:
{
clientID: "your-spotify-client-id",
scheme: "myapp",
host: "spotify-auth",
redirectPathPattern: "/auth/.*",
}Plugin options
| Option | Type | Required | Description |
| --------------------- | -------- | -------- | ---------------------------------------------------------- |
| clientID | string | ✅ | Your Spotify application's Client ID |
| scheme | string | ✅ | URL scheme registered for your app (e.g. "myapp") |
| host | string | ✅ | Host component of the redirect URI (e.g. "spotify-auth") |
| redirectPathPattern | string | — | Android redirect path regex. Defaults to ".*" |
The redirect URI registered in your Spotify Developer Dashboard must match {scheme}://{host} exactly (e.g. myapp://spotify-auth).
Usage
Typical integration: authenticate, connect App Remote, then read/control playback via hooks.
import { useEffect } from "react";
import {
Auth,
AppRemote,
AuthError,
AppRemoteError,
useSession,
useConnectionState,
useCurrentTrack,
useIsPlaying,
} from "@wwdrew/expo-spotify-sdk";
async function login() {
if (!Auth.isAvailable()) {
throw new Error("Install the Spotify app to continue");
}
// On iOS, clear a leaked in-flight auth before retrying.
await Auth.cancelPending();
return Auth.authenticate({
scopes: ["app-remote-control", "user-read-playback-state", "streaming"],
tokenSwapURL: "https://your-server.example.com/swap",
tokenRefreshURL: "https://your-server.example.com/refresh",
});
}
async function connectRemote(accessToken: string) {
try {
await AppRemote.connect(accessToken);
} catch (e) {
if (e instanceof AppRemoteError && e.code === "CONNECTION_FAILED") {
// Spotify's IPC transport is not ready — open Spotify and retry.
// See "App Remote connection failed" in Troubleshooting.
}
throw e;
}
}
function NowPlaying() {
const session = useSession();
const connectionState = useConnectionState();
const track = useCurrentTrack();
const isPlaying = useIsPlaying();
useEffect(() => {
if (session == null || connectionState !== "disconnected") return;
void connectRemote(session.accessToken);
}, [session, connectionState]);
if (connectionState !== "connected") {
return null;
}
return (
<Text>
{track?.name ?? "No track"} · {isPlaying ? "Playing" : "Paused"}
</Text>
);
}Omit tokenSwapURL / tokenRefreshURL to use the implicit TOKEN flow (iOS only for refresh; not recommended on Android — see below). For local development without a swap server, auth can still succeed on iOS; production apps should use the code + swap flow.
Check account tier (Web API)
App Remote does not expose Premium status. Call Spotify's Web API with the access token from Auth.authenticate():
const res = await fetch("https://api.spotify.com/v1/me", {
headers: { Authorization: `Bearer ${session.accessToken}` },
});
const profile = await res.json();
const isPremium = profile.product === "premium";The example app displays this as "Account tier: Premium / Free".
Auto-connect App Remote
Avoid reconnect loops by gating on connection state and attempting at most once per session restore:
import { useEffect, useRef } from "react";
import {
AppRemote,
useConnectionState,
useSession,
} from "@wwdrew/expo-spotify-sdk";
/**
* Connect when a session exists and App Remote is disconnected.
* Set `once` to true to avoid retry spam after failures (recommended on cold start).
*/
export function useAutoConnectAppRemote(options?: { once?: boolean }) {
const session = useSession();
const connectionState = useConnectionState();
const attemptedRef = useRef(false);
useEffect(() => {
const token = session?.accessToken;
if (!token || connectionState !== "disconnected") return;
if (options?.once && attemptedRef.current) return;
attemptedRef.current = true;
void AppRemote.connect(token).catch(() => {
// Surface via AppRemote.addListener("connectionError") or your UI.
});
}, [session?.accessToken, connectionState, options?.once]);
}For iOS, if connect() fails with CONNECTION_FAILED, foreground the Spotify app and retry manually — see Troubleshooting.
Spotify Premium and App Remote
| Concern | Auth (Auth.*) | App Remote (AppRemote.*, Player.*, …) |
| --------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------- |
| Spotify app installed | Recommended (native UX) | Required |
| Spotify app running | No | Required — connect talks to the running Spotify process over IPC |
| Spotify Premium | No | Required for reliable on-demand playback and rich player state (track name, transport, browse) |
| Free account | Auth works | Player.play() may fail with PREMIUM_REQUIRED; User.getCapabilities().canPlayOnDemand is false |
This library does not play audio. It remote-controls the official Spotify app. On Android especially, Free accounts often see empty or incomplete now-playing metadata even when connected.
Migration from v0.x
Removed in 2.x. The flat top-level functions (authenticateAsync, isAvailable, refreshSessionAsync, cancelPendingAuthAsync, addSessionChangeListener) and legacy type aliases (SpotifyConfig, SpotifyErrorCode) are no longer exported on the 2.x lane.
| v0.x | v2 |
| --- | --- |
| isAvailable() | Auth.isAvailable() |
| authenticateAsync(config) | Auth.authenticate(config) |
| refreshSessionAsync(config) | Auth.refresh(config) |
| cancelPendingAuthAsync() | Auth.cancelPending() |
| addSessionChangeListener(cb) | Auth.addListener("sessionChange", cb) |
| SpotifyError (auth throws) | AuthError (or instanceof SpotifyError as catch-all) |
| SpotifyErrorCode | AuthErrorCode |
Need the deprecated shims temporarily? Pin 1.x: npm install @wwdrew/expo-spotify-sdk@1.
SpotifySession, SpotifyScope, and config shapes are unchanged. For auth-specific e.code narrowing, use instanceof AuthError instead of instanceof SpotifyError.
Documentation
| Guide | Contents | | --- | --- | | API reference | All namespaces, methods, hooks | | Error codes | Per-namespace codes with when/what-to-do | | App Remote error mapping | iOS/Android native → JS mapping matrix | | Platform differences | iOS vs Android parity | | Token swap server | Swap/refresh endpoints + example app setup |
Troubleshooting
INVALID_CONFIG: Missing meta-data 'spotifyClientId'
Run expo prebuild after adding the plugin to your config. The plugin injects the required AndroidManifest.xml entries.
Auth.isAvailable() returns false on Android 11+ release builds
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.
iOS: authentication never returns
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.
AUTH_IN_PROGRESS
Auth.authenticate() was called while a previous call was still pending. Usually a concurrent call — wait for the first to settle.
On iOS this can also be a stuck state when Spotify never redirects back. Call Auth.cancelPending() before retrying.
App Remote: CONNECTION_FAILED / Connection refused (iOS code 61)
The Spotify app is installed but its App Remote transport is not accepting connections. Common causes:
- Spotify is not in the foreground — switch to Spotify, then retry
AppRemote.connect(). - Connect ran before auth finished — call
AppRemote.connect()only afterAuth.authenticate()resolves. - Stale access token — refresh or re-authenticate, then reconnect.
- Retry loop on startup — avoid calling
connect()on every render whileconnectionState === "disconnected"; gate on a one-shot flag or user action.
Token swap: NETWORK_ERROR / Could not connect to the server to http://127.0.0.1:…/swap
The swap URL must be reachable from the device running your app. During local dev, the Expo dev server must be running and serving API routes. Omit tokenSwapURL to test auth without a swap server (iOS TOKEN flow only; see Platform differences).
Now playing shows no track title (Android, Free account)
App Remote player state is limited for non-Premium users. Check GET /v1/me → product and User.getCapabilities().canPlayOnDemand.
Related docs
- CONTEXT.md — terminology (Auth SDK vs App Remote vs Web API)
- docs/api-reference.md — method reference
- docs/error-codes.md — error code tables
- docs/QA_CHECKLIST.md — manual QA before a
2.xrelease onmain(or1.xonv1) - docs/RELEASE.md — Release Please on
main(2.x); maintenance releases fromv1(1.x) - ATTRIBUTION.md — third-party SDKs and scope boundaries
Acknowledgements
Inspired by react-native-spotify-remote and expo-spotify.
Contributing
See CONTRIBUTING.md.
License
MIT — see LICENSE.
