diff --git a/packages/web/examples/upload/README.md b/packages/web/examples/upload/README.md index f88d81bee3e..ab2aca96bb0 100644 --- a/packages/web/examples/upload/README.md +++ b/packages/web/examples/upload/README.md @@ -4,15 +4,30 @@ A serverless Audius track upload example using SDK + OAuth PKCE entirely in the ## How it works -1. User clicks "Sign in with Audius" — `sdk.oauth.loginAsync({ scope: 'write' })` opens a popup, runs the PKCE flow, and stores the access token internally in the SDK's `tokenStore`. -2. User picks an audio file (and optional cover art), fills in title/genre/description. -3. On upload: +1. User clicks "Sign in with Audius" — `sdk.oauth.login({ scope: 'write', display: 'popup' })` opens a popup and runs the PKCE flow. The `redirectUri` is set once in the SDK config (see `src/sdk.ts`). +2. The popup redirects to Audius, then back to `redirectUri` (this same app) with an authorization code in the URL. +3. On the callback page (inside the popup), `sdk.oauth.handleRedirect()` detects `window.opener`, forwards the authorization code back to the parent window via `postMessage`, and closes the popup. +4. The parent's `login()` promise resolves; call `sdk.oauth.getUser()` to retrieve the authenticated user's profile. The access token is stored internally in the SDK's `tokenStore`. +5. User picks an audio file (and optional cover art), fills in title/genre/description. +6. On upload: - `sdk.uploads.createAudioUpload({ file })` uploads audio to a storage node → returns `trackCid`, `origFileCid`, `duration`, etc. - `sdk.uploads.createImageUpload({ file })` uploads cover art → returns `coverArtSizes` CID. - `sdk.tracks.createTrack({ userId, metadata })` registers the track on-chain, authenticated via the stored OAuth access token. ## Setup +### 1. Register the redirect URI + +In your developer app settings at **audius.co/settings → Developer Apps**, add the following redirect URI: + +``` +http://localhost:5177/ +``` + +This is recommended so the OAuth server will validate the callback URL when the popup redirects back to this app. For production deployments, register your deployed URL instead (e.g. `https://yourapp.com`). + +### 2. Configure and run + ```bash cp .env.example .env # Edit .env and set VITE_AUDIUS_API_KEY to your developer app API key diff --git a/packages/web/examples/upload/package-lock.json b/packages/web/examples/upload/package-lock.json index ec8c2baa73b..b6e95b1f4be 100644 --- a/packages/web/examples/upload/package-lock.json +++ b/packages/web/examples/upload/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@audius/web-example-oauth-upload", + "name": "@audius/web-example-upload", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@audius/web-example-oauth-upload", + "name": "@audius/web-example-upload", "version": "1.0.0", "dependencies": { "@audius/sdk": "file:../../../sdk", diff --git a/packages/web/examples/upload/src/App.tsx b/packages/web/examples/upload/src/App.tsx index aa33bc50580..b447d924f36 100644 --- a/packages/web/examples/upload/src/App.tsx +++ b/packages/web/examples/upload/src/App.tsx @@ -50,10 +50,49 @@ export default function App() { const audioInputRef = useRef(null) const coverInputRef = useRef(null) - // Re-initialize SDK on mount to pick up any stored OAuth state from a - // previous page load (e.g. after a full-page redirect OAuth flow). + // On mount: restore an existing session or handle an OAuth popup callback. useEffect(() => { - getSDK() + const sdk = getSDK() + + // If the URL contains ?code=&state=, this page is returning from an OAuth + // redirect. handleRedirect() handles both cases: + // - popup: window.opener exists → forwards the code to the parent + // and closes the popup. login() in the parent resolves. + // - fullScreen: no opener → performs the PKCE exchange locally and + // stores the tokens. Call getUser() afterwards to get the + // profile. + if (sdk.oauth.hasRedirectResult()) { + setLoading(true) + sdk.oauth + .handleRedirect() + .then(() => sdk.oauth.getUser()) + .then((user) => { + setProfile(user) + setScreen('signed-in') + }) + .catch((e: unknown) => { + setError(e instanceof Error ? e.message : 'Sign-in failed') + }) + .finally(() => setLoading(false)) + return + } + + // Otherwise, if a session is already stored from a previous login, restore + // the profile via getUser() so the user doesn't have to sign in again. + sdk.oauth.isAuthenticated().then((authenticated) => { + if (!authenticated) return + setLoading(true) + sdk.oauth + .getUser() + .then((user) => { + setProfile(user) + setScreen('signed-in') + }) + .catch(() => { + // Token may be expired — silently fall back to the sign-in screen. + }) + .finally(() => setLoading(false)) + }) }, []) const handleSignIn = useCallback(async () => { @@ -61,18 +100,19 @@ export default function App() { setLoading(true) try { const sdk = getSDK() - if (!sdk.oauth) { - setError('OAuth not available — make sure VITE_AUDIUS_API_KEY is set.') - return - } - // loginAsync with scope='write' triggers the PKCE flow. The SDK opens - // a popup, exchanges the auth code for tokens internally, and resolves - // with the user profile. No server required. - const { profile: p } = await sdk.oauth.loginAsync({ + // login opens a popup pointing to the Audius consent screen. The popup + // redirects to redirectUri (set in SDK config) after the user approves. + // handleRedirect() on that page detects window.opener, forwards the + // auth code back to this window, and closes the popup. + // + // To use a full-page redirect instead, change display to 'fullScreen'. + // login will navigate away; handleRedirect() on the next mount will + // complete the exchange and restore the signed-in state. + await sdk.oauth.login({ scope: 'write', - redirectUri: 'postMessage', display: 'popup' }) + const p = await sdk.oauth.getUser() setProfile(p) setScreen('signed-in') setResult(null) @@ -85,7 +125,7 @@ export default function App() { const handleSignOut = useCallback(async () => { const sdk = getSDK() - await sdk.oauth?.logout().catch(() => {}) + await sdk.oauth.logout().catch(() => {}) setProfile(null) setAudioFile(null) setCoverFile(null) diff --git a/packages/web/examples/upload/src/sdk.ts b/packages/web/examples/upload/src/sdk.ts index 503d83be351..005d1053325 100644 --- a/packages/web/examples/upload/src/sdk.ts +++ b/packages/web/examples/upload/src/sdk.ts @@ -10,19 +10,25 @@ let sdkInstance: AudiusSdk | null = null /** * Returns a singleton SDK instance initialised with the developer app API key. * The API key enables PKCE-based OAuth for the write scope so that - * sdk.oauth.loginAsync({ scope: 'write' }) stores an access token internally, + * sdk.oauth.login({ scope: 'write' }) stores an access token internally, * allowing sdk.tracks.createTrack to be called directly from the browser * without a backend server. */ export function getSDK(): AudiusSdk { if (!sdkInstance) { + // Use the current page as the redirect URI — the popup redirects here and + // handleRedirect() detects window.opener, forwards the code to the parent, + // and closes the popup. + const redirectUri = + window.location.origin + window.location.pathname sdkInstance = config.apiKey ? sdk({ appName: APP_NAME, apiKey: config.apiKey, + redirectUri, environment: config.environment }) - : sdk({ appName: APP_NAME, environment: config.environment }) + : sdk({ appName: APP_NAME, redirectUri, environment: config.environment }) } return sdkInstance }