UNPKG

@wwdrew/expo-spotify-sdk

Version:

Expo module wrapping the native Spotify iOS (v5) and Android (v4) SDKs for OAuth authentication and App Remote playback control

145 lines 5.52 kB
import { Platform } from "expo-modules-core"; import ExpoSpotifySDKModule from "../ExpoSpotifySDKModule"; import { AuthError } from "./error"; import { createNativeErrorRethrow } from "../internal/native-errors"; export { AuthError } from "./error"; // --------------------------------------------------------------------------- // Internal helpers // --------------------------------------------------------------------------- const ANDROID_TOKEN_FLOW_WARNING = "[expo-spotify-sdk] You are using Auth.authenticate on Android without a " + "tokenSwapURL. The Spotify Android SDK does NOT return a refresh token or " + "the actual granted scopes through this path; see the README's " + "'Android implicit (TOKEN) flow is not recommended' section."; let warnedAboutAndroidTokenFlow = false; const rethrowAsAuthError = createNativeErrorRethrow({ ErrorClass: AuthError, unknownCode: "UNKNOWN", validCodes: new Set([ "USER_CANCELLED", "AUTH_IN_PROGRESS", "INVALID_CONFIG", "NETWORK_ERROR", "TOKEN_SWAP_FAILED", "TOKEN_SWAP_PARSE_ERROR", "REFRESH_TOKEN_EXPIRED", "SPOTIFY_NOT_INSTALLED", "AUTH_ERROR", "UNKNOWN", ]), }); function normaliseSession(raw) { if (!raw || typeof raw !== "object") { throw new AuthError("UNKNOWN", "Native module returned a non-object session"); } const r = raw; const accessToken = r.accessToken; if (typeof accessToken !== "string" || accessToken.length === 0) { throw new AuthError("UNKNOWN", "Session is missing accessToken"); } const expirationDate = r.expirationDate; if (typeof expirationDate !== "number") { throw new AuthError("UNKNOWN", "Session is missing expirationDate"); } const refreshTokenRaw = r.refreshToken; const refreshToken = typeof refreshTokenRaw === "string" && refreshTokenRaw.length > 0 ? refreshTokenRaw : null; const scopesRaw = r.scopes; const scopes = Array.isArray(scopesRaw) ? scopesRaw.filter((s) => typeof s === "string") : []; return { accessToken, refreshToken, expirationDate, scopes }; } // --------------------------------------------------------------------------- // Auth namespace // --------------------------------------------------------------------------- /** * Spotify Auth namespace. Handles OAuth authentication and session lifecycle. * * @example * ```ts * import { Auth } from "@wwdrew/expo-spotify-sdk"; * * const session = await Auth.authenticate({ scopes: ["streaming"] }); * ``` */ export const Auth = { /** * Returns `true` if the Spotify app is installed on the device. */ isAvailable() { return ExpoSpotifySDKModule.isAvailable(); }, /** * Starts a Spotify OAuth flow. Resolves with a {@link SpotifySession}; * rejects with an {@link AuthError} carrying a `code`. */ authenticate(config) { if (!config.scopes || config.scopes.length === 0) { return Promise.reject(new AuthError("INVALID_CONFIG", "scopes must contain at least one entry")); } if (Platform.OS === "android" && !config.tokenSwapURL && !warnedAboutAndroidTokenFlow) { warnedAboutAndroidTokenFlow = true; console.warn(ANDROID_TOKEN_FLOW_WARNING); } return ExpoSpotifySDKModule.authenticateAsync(config) .then(normaliseSession) .catch(rethrowAsAuthError); }, /** * Exchanges a refresh token for a new access token via your token refresh * server. Resolves with a fresh {@link SpotifySession}; rejects with an * {@link AuthError}. */ refresh(config) { if (!config.refreshToken) { return Promise.reject(new AuthError("INVALID_CONFIG", "refreshToken is required")); } if (!config.tokenRefreshURL) { return Promise.reject(new AuthError("INVALID_CONFIG", "tokenRefreshURL is required")); } return ExpoSpotifySDKModule.refreshSessionAsync(config) .then(normaliseSession) .catch(rethrowAsAuthError); }, /** * Forcibly cancel any in-flight `Auth.authenticate()` call. No-op on * Android (the Android coordinator self-cleans via structured concurrency). * * Use before `Auth.authenticate()` to defensively clear any leaked iOS * coordinator state (the `SPTSessionManager` delegate callbacks are not * guaranteed to fire). */ cancelPending() { if (Platform.OS !== "ios") { return Promise.resolve(); } return ExpoSpotifySDKModule.cancelPendingAuthAsync(); }, /** * Subscribes to session lifecycle events. * * Events fire for every `Auth.authenticate()` and `Auth.refresh()` call, * regardless of whether the call was awaited. Useful for persisting tokens * in a central store without coupling the store to the call sites. * * Returns a `Subscription` — call `.remove()` to unsubscribe. * * @example * ```ts * const sub = Auth.addListener("sessionChange", (event) => { * if (event.type === "didInitiate" || event.type === "didRenew") { * store.setSession(event.session); * } * }); * // later: * sub.remove(); * ``` */ addListener(event, listener) { return ExpoSpotifySDKModule.addListener("onSessionChange", listener); }, }; //# sourceMappingURL=index.js.map