diff --git a/packages/react/README.md b/packages/react/README.md index bf0f38d5d..2d2873ccf 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -72,7 +72,7 @@ export const MyApp = (props) => { ## Example usage -```typescript +```tsx // import the API client for your specific application from your client package, see your app's installing instructions import { Client } from "@gadget-client/my-gadget-app"; // import the required Provider object and some example hooks from this package @@ -1039,3 +1039,94 @@ export const App = () => ( ); ``` +## Authentication + +When working with Gadget auth, there are several hooks and components that can help you manage the authentication state of your application. + +The `Provider` component exported from this library accepts an `auth` prop which can be used to configure the relative paths to your app's sign in and sign out endpoints. If you do not provide these paths, the default values of `/auth/signin` and `/auth/signout` will be used. + +The hooks use the Gadget client's `suspense: true` option, making it easier to manage the async nature of the hooks without having to deal with loading state. + +```tsx +import { Client } from "@gadget-client/my-gadget-app"; +import { Provider } from "@gadgetinc/react"; +import React, { Suspense } from "react"; +import App from "./App"; + +// instantiate the API client for our app +const api = new Client({ authenticationMode: { browserSession: true } }); + +export function main() { + // ensure any components which use the @gadgetinc/react hooks are wrapped with the Provider and a Suspense component + return ( + + Loading...}> + + + + ); +} +``` + +### Hooks + +React hooks are available to help you manage the authentication state of your application. + +### `useSession()` +Returns the current session, equivalent to `await api.currentSession.get()` or `useGet(api.currentSession)`, but uses Promises for an easier interface. Throws a Suspense promise while the session is being loaded + +### `useUser()` +Returns the current user of the session, if present. For unauthenticated sessions, returns `null`. Throws a Suspense promise while the session/user are loading + +### `useIsSignedIn()` +Returns true if the session is logged in (has a user associated with it), and false otherwise. Throws a Suspense promise while the session/user is loading + +```tsx +export default function App() { + const user = useUser(); + const isSignedIn = useIsSignedIn(); + const gadgetContext = useGadgetContext(); + + return ( + <> + { isSignedIn ?

Hello, {user.firstName} {user.lastName}}

: Please sign in } + + ); +} +``` + +### Components + +If you are trying to control the layout of your application based on authentication state, it may be helpful to use the Gadget auth React components instead of, or in addition to, the hooks. + +### `` +Conditionally renders its children if the current session has a user associated with it, similar to the `useIsSignedIn()` hook. + +```tsx +

Hello, human!

+``` + +### `` +Conditionally renders its children if the current session has a user associated with it, similar to the `useIsSignedIn()` hook. + +```tsx +Sign In! +``` + +### +Conditionally renders its children if the current session has a user associated with it, or redirects the browser via `window.location.assign` if the user is not currently signed in. This component is helpful for protecting front-end routes. + +```tsx + + + }> + } /> + + + + } /> + + + +``` diff --git a/packages/react/spec/auth/useUser.spec.ts b/packages/react/spec/auth/useUser.spec.ts index 41d4c650d..8990e5b52 100644 --- a/packages/react/spec/auth/useUser.spec.ts +++ b/packages/react/spec/auth/useUser.spec.ts @@ -24,4 +24,24 @@ describe("useUser", () => { rerender(); expect(result.current).toBe(null); }); + + test("it does something when the user fails to fetch", async () => { + const { result, rerender } = renderHook(() => useUser(), { wrapper: TestWrapper(superAuthApi) }); + + expect(mockUrqlClient.executeQuery).toBeCalledTimes(1); + mockUrqlClient.executeQuery.pushResponse("currentSession", { + data: { + currentSession: { + id: "123", + userId: null, + user: null, + }, + }, + stale: false, + hasNext: false, + }); + + rerender(); + expect(result.current).toBe(null); + }) }); diff --git a/packages/react/spec/components/SignedInOrRedirect.spec.tsx b/packages/react/spec/components/SignedInOrRedirect.spec.tsx index 743b7848b..d5ae1e4bd 100644 --- a/packages/react/spec/components/SignedInOrRedirect.spec.tsx +++ b/packages/react/spec/components/SignedInOrRedirect.spec.tsx @@ -6,7 +6,7 @@ import { TestWrapper } from "../../spec/testWrapper"; import { expectMockSignedInUser, expectMockSignedOutUser } from "../../spec/utils"; import { SignedInOrRedirect } from "../../src/components/SignedInOrRedirect"; -describe("SignedInOrRedirectOrRedirect", () => { +describe("SignedInOrRedirect", () => { const { location } = window; const mockAssign = jest.fn(); diff --git a/packages/react/src/auth/useIsSignedIn.ts b/packages/react/src/auth/useIsSignedIn.ts index efb824301..a848a894e 100644 --- a/packages/react/src/auth/useIsSignedIn.ts +++ b/packages/react/src/auth/useIsSignedIn.ts @@ -1,5 +1,9 @@ import { useUser } from "./useUser"; +/** + * Used for determining if the current `Session` has a user `User` associated with it (is signed in). Will suspend while the session is being fetched. + * @returns `true` if the `Session` has a `User`, `false` otherwise. + */ export const useIsSignedIn = () => { const user = useUser(); return !!user; diff --git a/packages/react/src/auth/useSession.ts b/packages/react/src/auth/useSession.ts index a5e6a518b..797d48575 100644 --- a/packages/react/src/auth/useSession.ts +++ b/packages/react/src/auth/useSession.ts @@ -31,6 +31,10 @@ const useGetSessionAndUser = () => { } }; +/** + * Used for fetching the current `Session` record from Gadget. Will suspend while the user is being fetched. + * @returns The current session + */ export const useSession = (): GadgetSession | undefined => { const [{ data: session }] = useGetSessionAndUser(); return session; diff --git a/packages/react/src/auth/useUser.ts b/packages/react/src/auth/useUser.ts index 25dfc319f..5dab1800d 100644 --- a/packages/react/src/auth/useUser.ts +++ b/packages/react/src/auth/useUser.ts @@ -1,5 +1,9 @@ import { useSession } from "./useSession"; +/** + * Used for fetching the current `User` record from Gadget. Will return `null` if the session is unauthenticated. Will suspend while the user is being fetched. + * @returns The current user associated with the session or `null`. + */ export const useUser = () => { const session = useSession(); return session && session.user; diff --git a/packages/react/src/components/SignedIn.tsx b/packages/react/src/components/SignedIn.tsx index 6cadfc118..5245c134a 100644 --- a/packages/react/src/components/SignedIn.tsx +++ b/packages/react/src/components/SignedIn.tsx @@ -3,6 +3,9 @@ import React from "react"; import { useSession } from "../../src/auth/useSession"; import { isSessionSignedIn } from "../../src/auth/utils"; +/** + * Renders its `children` if the current `Session` is signed in (has an associated `User`), otherwise renders nothing. + */ export const SignedIn = (props: { children: ReactNode }) => { const session = useSession(); if (session && isSessionSignedIn(session)) { diff --git a/packages/react/src/components/SignedInOrRedirect.tsx b/packages/react/src/components/SignedInOrRedirect.tsx index 0853273f5..28f2b2bb3 100644 --- a/packages/react/src/components/SignedInOrRedirect.tsx +++ b/packages/react/src/components/SignedInOrRedirect.tsx @@ -4,6 +4,10 @@ import { GadgetClientContext } from "../../src/GadgetProvider"; import { useSession } from "../../src/auth/useSession"; import { isSessionSignedIn } from "../../src/auth/utils"; +/** +/** + * Renders its `children` if the current `Session` is signed in, otherwise redirects the browser to the `signInPath` configured in the `Provider`. Uses `window.location.assign` to perform the redirect. + */ export const SignedInOrRedirect = (props: { children: ReactNode }) => { const [redirected, setRedirected] = useState(false); const session = useSession(); diff --git a/packages/react/src/components/SignedOut.tsx b/packages/react/src/components/SignedOut.tsx index 017fcb404..516d3dc50 100644 --- a/packages/react/src/components/SignedOut.tsx +++ b/packages/react/src/components/SignedOut.tsx @@ -3,6 +3,9 @@ import React from "react"; import { useSession } from "../../src/auth/useSession"; import { isSessionSignedOut } from "../../src/auth/utils"; +/** + * Renders its `children` if the current `Session` is signed out (no associated `User`), otherwise renders nothing. + */ export const SignedOut = (props: { children: ReactNode }) => { const session = useSession(); if (!session || isSessionSignedOut(session)) {