npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@beatsphere/expo-spotify-remote

v0.1.0

Published

High-level Spotify App Remote wrapper for Expo — authentication, now playing detection, lifecycle management, and token refresh with battle-tested retry logic.

Readme

@beatsphere/expo-spotify-remote

High-level Spotify App Remote wrapper for Expo/React Native. Battle-tested in BeatSphere.

Built on top of @42techpacks/expo-spotify-sdk, this package adds:

  • OAuth authentication with token swap/refresh
  • Now playing detection with retry logic and platform-specific handling
  • App Remote lifecycle management (iOS background/foreground)
  • Token caching with automatic refresh
  • Ad detection and filtering
  • Local play history with dedup and FIFO eviction
  • Listening status — live vs recently played
  • SecureStore wrapper with iOS Keychain and Android Keystore error recovery
  • User profile fetching (Web API + native fallback)

Install

npm install @beatsphere/expo-spotify-remote @42techpacks/expo-spotify-sdk expo-secure-store

Optional (for play history):

npm install @react-native-async-storage/async-storage

Android App Remote Setup

The base @42techpacks/expo-spotify-sdk doesn't include Android App Remote support. This package ships the Spotify App Remote AAR and a patch to enable it.

  1. Install patch-package:

    npm install patch-package --save-dev
  2. Copy the patch to your project:

    cp node_modules/@beatsphere/expo-spotify-remote/patches/@42techpacks+expo-spotify-sdk+0.5.6.patch patches/
  3. Add to your package.json scripts:

    {
      "scripts": {
        "postinstall": "patch-package"
      }
    }
  4. Run: npx patch-package

Expo Plugin

Add the Spotify SDK plugin to your app.config.js or app.json:

// app.config.js
export default {
  plugins: [
    [
      '@42techpacks/expo-spotify-sdk',
      {
        scheme: 'myapp',
        host: 'spotify-callback',
        clientID: process.env.EXPO_PUBLIC_SPOTIFY_CLIENT_ID,
      },
    ],
  ],
};

Quick Start

1. Configure (once, in app root)

// app/_layout.tsx
import { configure, initLifecycle } from '@beatsphere/expo-spotify-remote';
import { useEffect } from 'react';

configure({
  clientID: process.env.EXPO_PUBLIC_SPOTIFY_CLIENT_ID!,
  redirectURL: 'myapp://spotify-callback',
  tokenSwapURL: 'https://api.myapp.com/auth/spotify/swap',
  tokenRefreshURL: 'https://api.myapp.com/auth/spotify/refresh',
});

export default function RootLayout() {
  useEffect(() => {
    initLifecycle();
  }, []);

  return <Slot />;
}

2. Authenticate

import { authenticate, isSpotifyAppInstalled, openSpotifyStore } from '@beatsphere/expo-spotify-remote';

async function login() {
  const installed = await isSpotifyAppInstalled();
  if (!installed) {
    await openSpotifyStore();
    return;
  }

  try {
    const session = await authenticate();
    console.log('Authenticated!', session.accessToken);
  } catch (err) {
    if (err.message === 'SPOTIFY_APP_NOT_INSTALLED') {
      await openSpotifyStore();
    }
  }
}

3. Get Now Playing

import { getNowPlaying } from '@beatsphere/expo-spotify-remote';

const track = await getNowPlaying();
if (track) {
  console.log(`${track.name} by ${track.artist}`);
  console.log(`Art: ${track.imageUrl}`);
}

4. Listening Status (live + recent)

import { getListeningStatus } from '@beatsphere/expo-spotify-remote';

const status = await getListeningStatus();
if (status) {
  console.log(`${status.track.name} — ${status.status}`); // 'live' or 'recent'
}

5. User Profile

import { getUser } from '@beatsphere/expo-spotify-remote';

const user = await getUser();
// { id: 'spotify_user_id', name: 'Display Name', email: '...', imageUrl: '...' }

Configuration Options

configure({
  // Required
  clientID: string;
  redirectURL: string;
  tokenSwapURL: string;
  tokenRefreshURL: string;

  // Optional
  scopes?: string[];                // Default: standard playback + user scopes
  authTimeoutMs?: number;           // Default: 30000
  maxHistorySize?: number;          // Default: 50
  recentThresholdSeconds?: number;  // Default: 1200 (20 min)
  storageKeyPrefix?: string;        // Default: 'spotify_remote_'

  // Custom token refresh (e.g. via your backend with JWT auth)
  onTokenRefresh?: () => Promise<{
    accessToken: string;
    refreshToken?: string;
    expiresIn?: number;
  } | null>;

  // Logging (pass `console` for basic output)
  logger?: {
    info?: (msg: string, data?: object) => void;
    warn?: (msg: string, data?: object) => void;
    error?: (msg: string, data?: object) => void;
  };
});

Backend Requirements

Your server must implement two endpoints for Spotify's token exchange:

POST /auth/spotify/swap

Called during initial authentication. Receives the authorization code and exchanges it for tokens.

Request body:

{ "code": "<authorization_code>" }

Response:

{
  "access_token": "...",
  "refresh_token": "...",
  "expires_in": 3600
}

POST /auth/spotify/refresh

Called when the access token expires.

Request body:

{ "refresh_token": "<refresh_token>" }

Response:

{
  "access_token": "...",
  "expires_in": 3600
}

See the Spotify Authorization Guide for implementation details. The key requirement is that your client secret stays server-side.

API Reference

Authentication

| Function | Description | |----------|-------------| | configure(config) | Initialize the module (call once) | | authenticate() | Run full OAuth flow, returns SpotifySession | | isSpotifyAppInstalled() | Check if Spotify is on the device | | openSpotifyStore() | Open App Store / Play Store |

Playback

| Function | Description | |----------|-------------| | getNowPlaying() | Get currently playing track (or null) | | getListeningStatus() | Get live or recent listening status | | isSpotifyAd(track) | Check if a track is an ad |

App Remote

| Function | Description | |----------|-------------| | connectRemote() | Connect App Remote manually | | disconnectRemote() | Disconnect App Remote | | isRemoteConnected() | Check connection status | | initLifecycle(isSpotifyUser?) | Start iOS lifecycle management | | destroyLifecycle() | Stop lifecycle management |

User & History

| Function | Description | |----------|-------------| | getUser() | Get Spotify user profile | | getRecentHistory() | Get local play history | | storeTrackHistory(track) | Manually add to history | | clearHistory() | Clear local history |

Token Management

| Function | Description | |----------|-------------| | getValidAccessToken() | Get a non-expired token | | clearTokenCache() | Clear in-memory token cache |

Storage Utilities

| Function | Description | |----------|-------------| | getSecureItem(key) | Read from SecureStore with retry | | setSecureItem(key, value) | Write to SecureStore with retry | | deleteSecureItem(key) | Delete from SecureStore | | parseImageUri(uri) | Convert Spotify image URI to CDN URL |

Platform Notes

iOS

  • App Remote requires the Spotify app to be installed
  • Lifecycle management (disconnect on background, reconnect on foreground) is handled automatically via initLifecycle()
  • If Spotify is suspended, getNowPlaying() uses authorizeAndPlayURI("") as a fallback to wake it
  • Player state fetching uses 5 retry attempts with increasing delays

Android

  • Requires the App Remote AAR + patch (see setup above)
  • Player state fetching uses 1 attempt (more reliable than iOS)
  • Android Keystore encryption errors are handled with automatic retry

License

MIT