diff --git a/docs/fern/docs/pages/concepts/oauth.mdx b/docs/fern/docs/pages/concepts/oauth.mdx index 185b05e40..8d08bc352 100644 --- a/docs/fern/docs/pages/concepts/oauth.mdx +++ b/docs/fern/docs/pages/concepts/oauth.mdx @@ -7,7 +7,13 @@ With Stack, users can connect multiple OAuth accounts, enabling access to provid ## Connecting with an OAuth Provider Post Sign-in -Users can connect with an OAuth provider during sign-in or after signing in using other methods. For the latter, use the `user.useConnectedAccount('', { or: 'redirect' })` method. If the account is already connected, the method returns a Connection object; otherwise, it redirects the user to the provider's authorization page. Here’s how to connect with Google: +Users can connect with an OAuth provider during sign-in or after signing in using other methods. For the latter, use the `user.useConnectedAccount('', { or: 'redirect' })` method. If the account is already connected, the method returns a Connection object; otherwise, it redirects the user to the provider's authorization page. + + + In order to connect accounts after sign-in, you need to set up your own OAuth client ID and client secret with the provider. See more details in the section [Going to Production](../getting-started/production#oauth-providers) + + +Here's how to connect with Google: ```jsx 'use client'; @@ -25,7 +31,7 @@ export default function Page() { ## Connecting with Extra Scopes -You can request extra scopes when connecting with an OAuth provider. For instance, to access Google Drive, pass the `https://www.googleapis.com/auth/drive` scope. Here’s an example: +You can request extra scopes when connecting with an OAuth provider. For instance, to access Google Drive, pass the `https://www.googleapis.com/auth/drive.readonly` scope. Here's an example: ```jsx 'use client'; @@ -35,7 +41,7 @@ import { useUser } from "@stackframe/stack"; export default function Page() { const user = useUser({ or: 'redirect' }); // Redirects to the Google authorization page, requesting access to Google Drive - const account = user.useConnectedAccount('google', { or: 'redirect' }); + const account = user.useConnectedAccount('google', { or: 'redirect', scopes: ['https://www.googleapis.com/authdrive.readonly'] }); // Account is always defined because of the redirect return
Google Drive connected
; } @@ -43,7 +49,7 @@ export default function Page() { ## Retrieving the Access Token -Once connected with an OAuth provider, obtain the access token using the `account.getAccessToken()` method. Use this token to access the provider’s API endpoints. Here’s an example of using the access token to interact with Google Drive API: +Once connected with an OAuth provider, obtain the access token using the `account.getAccessToken()` method. Use this token to access the provider's API endpoints. Here's an example of using the access token to interact with Google Drive API: ```jsx 'use client'; @@ -53,18 +59,17 @@ import { useUser } from "@stackframe/stack"; export default function Page() { const user = useUser({ or: 'redirect' }); - const account = user.useConnectedAccount('google', { or: 'redirect' }); - const tokens = account.getAccessToken(); - const [response, setResponse] = useState(null); + const account = user.useConnectedAccount('google', { or: 'redirect', scopes: ['https://www.googleapis.com/auth/drive.readonly'] }); + const tokens = account.useAccessToken(); + const [response, setResponse] = useState(); useEffect(() => { fetch('https://www.googleapis.com/drive/v3/files', { - headers: { - Authorization: `Bearer ${tokens.access_token}` - } + headers: { Authorization: `Bearer ${tokens.accessToken}` } }) - .then(res => res.json()) - .then(data => setResponse(data)); + .then((res) => res.json()) + .then((data) => setResponse(data)) + .catch((err) => console.error(err)); }, [tokens]); return
{response ? JSON.stringify(response) : 'Loading...'}
; @@ -83,7 +88,7 @@ Configure the `StackServerApp` with the required scopes: export const stackServerApp = new StackServerApp({ // your other settings ... oauthScopesOnSignIn: { - google: ['https://www.googleapis.com/auth/drive'] + google: ['https://www.googleapis.com/authdrive.readonly'] } }); ``` diff --git a/examples/custom-pages-example/src/app/drive/page.tsx b/examples/custom-pages-example/src/app/drive/page.tsx new file mode 100644 index 000000000..e1ff3550d --- /dev/null +++ b/examples/custom-pages-example/src/app/drive/page.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useUser } from "@stackframe/stack"; + +export default function Page() { + const user = useUser({ or: 'redirect' }); + const account = user.useConnectedAccount('google', { or: 'redirect', scopes: ['https://www.googleapis.com/auth/drive.readonly'] }); + const tokens = account.useAccessToken(); + const [response, setResponse] = useState(); + + useEffect(() => { + fetch('https://www.googleapis.com/drive/v3/files', { + headers: { Authorization: `Bearer ${tokens.accessToken}` } + }) + .then((res) => res.json()) + .then((data) => setResponse(data)) + .catch((err) => console.error(err)); + }, [tokens]); + + return
{response ? JSON.stringify(response) : 'Loading...'}
; +} \ No newline at end of file diff --git a/packages/stack-shared/src/hooks/use-trigger.tsx b/packages/stack-shared/src/hooks/use-trigger.tsx deleted file mode 100644 index 9c814da5e..000000000 --- a/packages/stack-shared/src/hooks/use-trigger.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from "react"; - -export function useTrigger(callback: () => void) { - const [hasTriggered, setHasTriggered] = React.useState(false); - React.useEffect(() => { - if (hasTriggered) { - callback(); - } - }, [hasTriggered]); - return () => setHasTriggered(true); -} diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts index 85d386529..62326a707 100644 --- a/packages/stack/src/lib/stack-app.ts +++ b/packages/stack/src/lib/stack-app.ts @@ -23,7 +23,6 @@ import { scrambleDuringCompileTime } from "@stackframe/stack-shared/dist/utils/c import { isReactServer } from "@stackframe/stack-sc"; import * as cookie from "cookie"; import { InternalSession } from "@stackframe/stack-shared/dist/sessions"; -import { useTrigger } from "@stackframe/stack-shared/dist/hooks/use-trigger"; import { mergeScopeStrings } from "@stackframe/stack-shared/dist/utils/strings"; @@ -990,14 +989,13 @@ class _StackClientAppImpl router.replace(this.urls.signIn)); if (userJson === null) { switch (options?.or) { case 'redirect': { // Updating the router is not allowed during the component render function, so we do it in a different async tick // The error would be: "Cannot update a component (`Router`) while rendering a different component." - triggerRedirectToSignIn(); + setTimeout(() => router.replace(this.urls.signIn), 0); suspend(); throw new StackAssertionError("suspend should never return"); } @@ -1535,14 +1533,13 @@ class _StackServerAppImpl router.replace(this.urls.signIn)); if (userJson === null) { switch (options?.or) { case 'redirect': { // Updating the router is not allowed during the component render function, so we do it in a different async tick // The error would be: "Cannot update a component (`Router`) while rendering a different component." - triggerRedirectToSignIn(); + setTimeout(() => router.replace(this.urls.signIn), 0); suspend(); throw new StackAssertionError("suspend should never return"); }