{"_id":"@beatsphere/expo-spotify-remote","name":"@beatsphere/expo-spotify-remote","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@beatsphere/expo-spotify-remote","version":"0.1.0","description":"High-level Spotify App Remote wrapper for Expo — authentication, now playing detection, lifecycle management, and token refresh with battle-tested retry logic.","main":"dist/index.js","types":"dist/index.d.ts","scripts":{"build":"tsc","prepublishOnly":"tsc","postinstall":"node scripts/postinstall.js"},"keywords":["expo","spotify","react-native","app-remote","now-playing","music","streaming","authentication"],"author":{"name":"BeatSphere","email":"hello@beatsphere.app"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/Beatsphere/expo-spotify-remote.git"},"bugs":{"url":"https://github.com/Beatsphere/expo-spotify-remote/issues"},"homepage":"https://github.com/Beatsphere/expo-spotify-remote#readme","peerDependencies":{"@42techpacks/expo-spotify-sdk":">=0.5.0","@react-native-async-storage/async-storage":">=2.0.0","expo":">=51.0.0","expo-secure-store":">=13.0.0","react-native":">=0.73.0"},"peerDependenciesMeta":{"@react-native-async-storage/async-storage":{"optional":true}},"devDependencies":{"@42techpacks/expo-spotify-sdk":"^0.5.6","@react-native-async-storage/async-storage":"^2.1.2","expo-secure-store":"^14.2.2","react-native":"^0.76.0","typescript":"^5.5.0"},"gitHead":"dfb644b9ae1702a9ee4398a551ac7280ae34dcc1","_id":"@beatsphere/expo-spotify-remote@0.1.0","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-pyn+iGyJT81+ZclSgDRqfsdv6fKLEqKJVRmCZhxEHmV5Q0zRMikG8zf0bmic9DjfYJGRMEacVaHybfYid5gmTQ==","shasum":"373890ce15d584b4b5d639d0e8e5774ac8b280eb","tarball":"https://registry.npmjs.org/@beatsphere/expo-spotify-remote/-/expo-spotify-remote-0.1.0.tgz","fileCount":58,"unpackedSize":354363,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQD6BDcIvh3PCgKszCL11BTcGP/Cwu8td7m9jepH+5PYAQIhAPIS42x/AY4GFzY3MBaqhdiG+RCpZ39saLoys0t653CF"}]},"_npmUser":{"name":"ayushh2k","email":"rustichouse042@gmail.com"},"directories":{},"maintainers":[{"name":"ayushh2k","email":"rustichouse042@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/expo-spotify-remote_0.1.0_1776967458811_0.04142537402575841"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-23T18:04:18.724Z","0.1.0":"2026-04-23T18:04:18.997Z","modified":"2026-04-23T18:04:19.238Z"},"maintainers":[{"name":"ayushh2k","email":"rustichouse042@gmail.com"}],"description":"High-level Spotify App Remote wrapper for Expo — authentication, now playing detection, lifecycle management, and token refresh with battle-tested retry logic.","homepage":"https://github.com/Beatsphere/expo-spotify-remote#readme","keywords":["expo","spotify","react-native","app-remote","now-playing","music","streaming","authentication"],"repository":{"type":"git","url":"git+https://github.com/Beatsphere/expo-spotify-remote.git"},"author":{"name":"BeatSphere","email":"hello@beatsphere.app"},"bugs":{"url":"https://github.com/Beatsphere/expo-spotify-remote/issues"},"license":"MIT","readme":"# @beatsphere/expo-spotify-remote\n\nHigh-level Spotify App Remote wrapper for Expo/React Native. Battle-tested in [BeatSphere](https://beatsphere.live).\n\nBuilt on top of [`@42techpacks/expo-spotify-sdk`](https://github.com/nichochar/expo-spotify-sdk), this package adds:\n\n- **OAuth authentication** with token swap/refresh\n- **Now playing detection** with retry logic and platform-specific handling\n- **App Remote lifecycle management** (iOS background/foreground)\n- **Token caching** with automatic refresh\n- **Ad detection** and filtering\n- **Local play history** with dedup and FIFO eviction\n- **Listening status** — live vs recently played\n- **SecureStore wrapper** with iOS Keychain and Android Keystore error recovery\n- **User profile** fetching (Web API + native fallback)\n\n## Install\n\n```bash\nnpm install @beatsphere/expo-spotify-remote @42techpacks/expo-spotify-sdk expo-secure-store\n```\n\nOptional (for play history):\n```bash\nnpm install @react-native-async-storage/async-storage\n```\n\n### Android App Remote Setup\n\nThe 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.\n\n1. Install [patch-package](https://github.com/ds300/patch-package):\n   ```bash\n   npm install patch-package --save-dev\n   ```\n\n2. Copy the patch to your project:\n   ```bash\n   cp node_modules/@beatsphere/expo-spotify-remote/patches/@42techpacks+expo-spotify-sdk+0.5.6.patch patches/\n   ```\n\n3. Add to your `package.json` scripts:\n   ```json\n   {\n     \"scripts\": {\n       \"postinstall\": \"patch-package\"\n     }\n   }\n   ```\n\n4. Run: `npx patch-package`\n\n### Expo Plugin\n\nAdd the Spotify SDK plugin to your `app.config.js` or `app.json`:\n\n```js\n// app.config.js\nexport default {\n  plugins: [\n    [\n      '@42techpacks/expo-spotify-sdk',\n      {\n        scheme: 'myapp',\n        host: 'spotify-callback',\n        clientID: process.env.EXPO_PUBLIC_SPOTIFY_CLIENT_ID,\n      },\n    ],\n  ],\n};\n```\n\n## Quick Start\n\n### 1. Configure (once, in app root)\n\n```tsx\n// app/_layout.tsx\nimport { configure, initLifecycle } from '@beatsphere/expo-spotify-remote';\nimport { useEffect } from 'react';\n\nconfigure({\n  clientID: process.env.EXPO_PUBLIC_SPOTIFY_CLIENT_ID!,\n  redirectURL: 'myapp://spotify-callback',\n  tokenSwapURL: 'https://api.myapp.com/auth/spotify/swap',\n  tokenRefreshURL: 'https://api.myapp.com/auth/spotify/refresh',\n});\n\nexport default function RootLayout() {\n  useEffect(() => {\n    initLifecycle();\n  }, []);\n\n  return <Slot />;\n}\n```\n\n### 2. Authenticate\n\n```tsx\nimport { authenticate, isSpotifyAppInstalled, openSpotifyStore } from '@beatsphere/expo-spotify-remote';\n\nasync function login() {\n  const installed = await isSpotifyAppInstalled();\n  if (!installed) {\n    await openSpotifyStore();\n    return;\n  }\n\n  try {\n    const session = await authenticate();\n    console.log('Authenticated!', session.accessToken);\n  } catch (err) {\n    if (err.message === 'SPOTIFY_APP_NOT_INSTALLED') {\n      await openSpotifyStore();\n    }\n  }\n}\n```\n\n### 3. Get Now Playing\n\n```tsx\nimport { getNowPlaying } from '@beatsphere/expo-spotify-remote';\n\nconst track = await getNowPlaying();\nif (track) {\n  console.log(`${track.name} by ${track.artist}`);\n  console.log(`Art: ${track.imageUrl}`);\n}\n```\n\n### 4. Listening Status (live + recent)\n\n```tsx\nimport { getListeningStatus } from '@beatsphere/expo-spotify-remote';\n\nconst status = await getListeningStatus();\nif (status) {\n  console.log(`${status.track.name} — ${status.status}`); // 'live' or 'recent'\n}\n```\n\n### 5. User Profile\n\n```tsx\nimport { getUser } from '@beatsphere/expo-spotify-remote';\n\nconst user = await getUser();\n// { id: 'spotify_user_id', name: 'Display Name', email: '...', imageUrl: '...' }\n```\n\n## Configuration Options\n\n```ts\nconfigure({\n  // Required\n  clientID: string;\n  redirectURL: string;\n  tokenSwapURL: string;\n  tokenRefreshURL: string;\n\n  // Optional\n  scopes?: string[];                // Default: standard playback + user scopes\n  authTimeoutMs?: number;           // Default: 30000\n  maxHistorySize?: number;          // Default: 50\n  recentThresholdSeconds?: number;  // Default: 1200 (20 min)\n  storageKeyPrefix?: string;        // Default: 'spotify_remote_'\n\n  // Custom token refresh (e.g. via your backend with JWT auth)\n  onTokenRefresh?: () => Promise<{\n    accessToken: string;\n    refreshToken?: string;\n    expiresIn?: number;\n  } | null>;\n\n  // Logging (pass `console` for basic output)\n  logger?: {\n    info?: (msg: string, data?: object) => void;\n    warn?: (msg: string, data?: object) => void;\n    error?: (msg: string, data?: object) => void;\n  };\n});\n```\n\n## Backend Requirements\n\nYour server must implement two endpoints for Spotify's token exchange:\n\n### POST `/auth/spotify/swap`\n\nCalled during initial authentication. Receives the authorization code and exchanges it for tokens.\n\n**Request body:**\n```json\n{ \"code\": \"<authorization_code>\" }\n```\n\n**Response:**\n```json\n{\n  \"access_token\": \"...\",\n  \"refresh_token\": \"...\",\n  \"expires_in\": 3600\n}\n```\n\n### POST `/auth/spotify/refresh`\n\nCalled when the access token expires.\n\n**Request body:**\n```json\n{ \"refresh_token\": \"<refresh_token>\" }\n```\n\n**Response:**\n```json\n{\n  \"access_token\": \"...\",\n  \"expires_in\": 3600\n}\n```\n\nSee the [Spotify Authorization Guide](https://developer.spotify.com/documentation/web-api/tutorials/code-flow) for implementation details. The key requirement is that your **client secret** stays server-side.\n\n## API Reference\n\n### Authentication\n| Function | Description |\n|----------|-------------|\n| `configure(config)` | Initialize the module (call once) |\n| `authenticate()` | Run full OAuth flow, returns `SpotifySession` |\n| `isSpotifyAppInstalled()` | Check if Spotify is on the device |\n| `openSpotifyStore()` | Open App Store / Play Store |\n\n### Playback\n| Function | Description |\n|----------|-------------|\n| `getNowPlaying()` | Get currently playing track (or null) |\n| `getListeningStatus()` | Get live or recent listening status |\n| `isSpotifyAd(track)` | Check if a track is an ad |\n\n### App Remote\n| Function | Description |\n|----------|-------------|\n| `connectRemote()` | Connect App Remote manually |\n| `disconnectRemote()` | Disconnect App Remote |\n| `isRemoteConnected()` | Check connection status |\n| `initLifecycle(isSpotifyUser?)` | Start iOS lifecycle management |\n| `destroyLifecycle()` | Stop lifecycle management |\n\n### User & History\n| Function | Description |\n|----------|-------------|\n| `getUser()` | Get Spotify user profile |\n| `getRecentHistory()` | Get local play history |\n| `storeTrackHistory(track)` | Manually add to history |\n| `clearHistory()` | Clear local history |\n\n### Token Management\n| Function | Description |\n|----------|-------------|\n| `getValidAccessToken()` | Get a non-expired token |\n| `clearTokenCache()` | Clear in-memory token cache |\n\n### Storage Utilities\n| Function | Description |\n|----------|-------------|\n| `getSecureItem(key)` | Read from SecureStore with retry |\n| `setSecureItem(key, value)` | Write to SecureStore with retry |\n| `deleteSecureItem(key)` | Delete from SecureStore |\n| `parseImageUri(uri)` | Convert Spotify image URI to CDN URL |\n\n## Platform Notes\n\n### iOS\n- App Remote requires the Spotify app to be installed\n- Lifecycle management (disconnect on background, reconnect on foreground) is handled automatically via `initLifecycle()`\n- If Spotify is suspended, `getNowPlaying()` uses `authorizeAndPlayURI(\"\")` as a fallback to wake it\n- Player state fetching uses 5 retry attempts with increasing delays\n\n### Android\n- Requires the App Remote AAR + patch (see setup above)\n- Player state fetching uses 1 attempt (more reliable than iOS)\n- Android Keystore encryption errors are handled with automatic retry\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-a33edf0666b22468c80e2bd8e9827f5e"}