Skip to content

Commit

Permalink
feat: add useSession hook (#242)
Browse files Browse the repository at this point in the history
Co-authored-by: Miłosz <[email protected]>
  • Loading branch information
jonas-jonas and mszekiel authored Oct 22, 2024
1 parent d23bb7c commit 06d876f
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 2 deletions.
32 changes: 31 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/elements-react/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ module.exports = {
caughtErrorsIgnorePattern: "^_",
},
],
// TODO(jonas): prettier and eslint sometimes disagree about this
"no-extra-semi": "off",
},
env: {
jest: true,
Expand Down
33 changes: 33 additions & 0 deletions packages/elements-react/api-report/elements-react.api.json
Original file line number Diff line number Diff line change
Expand Up @@ -2977,6 +2977,39 @@
"parameters": [],
"name": "useOryFlow"
},
{
"kind": "Function",
"canonicalReference": "@ory/elements-react!useSession:function(1)",
"docComment": "/**\n * A hook to get the current session from the Ory Network.\n *\n * Usage:\n * ```ts\n * const { session, error, isLoading } = useSession()\n * ```\n *\n * @returns The current session, error and loading state.\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "useSession: () => "
},
{
"kind": "Content",
"text": "{\n session: "
},
{
"kind": "Reference",
"text": "Session",
"canonicalReference": "@ory/client-fetch!Session:interface"
},
{
"kind": "Content",
"text": " | undefined;\n error: string | undefined;\n isLoading: boolean;\n}"
}
],
"fileUrlPath": "dist/index.d.ts",
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 4
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [],
"name": "useSession"
},
{
"kind": "TypeAlias",
"canonicalReference": "@ory/elements-react!VerificationFlowContainer:type",
Expand Down
8 changes: 8 additions & 0 deletions packages/elements-react/api-report/elements-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { PropsWithChildren } from 'react';
import * as react_jsx_runtime from 'react/jsx-runtime';
import { RecoveryFlow } from '@ory/client-fetch';
import { RegistrationFlow } from '@ory/client-fetch';
import { Session } from '@ory/client-fetch';
import { SettingsFlow } from '@ory/client-fetch';
import { UiNode } from '@ory/client-fetch';
import { UiNodeAnchorAttributes } from '@ory/client-fetch';
Expand Down Expand Up @@ -315,6 +316,13 @@ export function useNodeSorter(): (a: UiNode, b: UiNode, ctx: {
// @public
export function useOryFlow(): FlowContextValue;

// @public
export const useSession: () => {
session: Session | undefined;
error: string | undefined;
isLoading: boolean;
};

// @public
export type VerificationFlowContainer = OryFlow<FlowType.Verification, VerificationFlow>;

Expand Down
3 changes: 2 additions & 1 deletion packages/elements-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"lodash.merge": "4.6.2",
"react-hook-form": "7.53.0",
"react-intl": "6.7.0",
"tailwind-merge": "2.5.2"
"tailwind-merge": "2.5.2",
"zustand": "5.0.0"
},
"peerDependencies": {
"react": "18.3.1",
Expand Down
4 changes: 4 additions & 0 deletions packages/elements-react/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

export { useSession } from "./useSession"
162 changes: 162 additions & 0 deletions packages/elements-react/src/hooks/useSession.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

// useSession.test.tsx

import { Session } from "@ory/client-fetch"
import "@testing-library/jest-dom"
import "@testing-library/jest-dom/jest-globals"
import { act, render, screen, waitFor } from "@testing-library/react"
import { useOryFlow } from "../context/flow-context"
import { frontendClient } from "../util/client"
import { sessionStore, useSession } from "./useSession"

// Mock the necessary imports
jest.mock("../context/flow-context", () => ({
useOryFlow: jest.fn(),
}))

jest.mock("../util/client", () => ({
frontendClient: jest.fn(() => ({
toSession: jest.fn(),
})),
}))

// Create a test component to use the hook
const TestComponent = () => {
const { session, isLoading, error } = useSession()

if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
if (session) return <div>Session: {session.id}</div>

return <div>No session</div>
}

describe("useSession", () => {
const mockSession: Session = {
id: "test-session-id",
identity: {
id: "test-identity-id",
traits: {},
schema_id: "",
schema_url: "",
},
expires_at: new Date(),
}
const mockConfig = {
sdk: { url: "https://mock-sdk-url" },
}

beforeEach(() => {
jest.clearAllMocks()
// Mock the flow context
;(useOryFlow as jest.Mock).mockReturnValue({ config: mockConfig })
sessionStore.setState({
isLoading: false,
session: undefined,
error: undefined,
})
})

it("fetches and sets session successfully", async () => {
;(frontendClient as jest.Mock).mockReturnValue({
toSession: jest.fn().mockResolvedValue(mockSession),
})

render(<TestComponent />)

// Initially, it should show loading
expect(screen.getByText("Loading...")).toBeInTheDocument()

// Wait for the hook to update
await waitFor(() =>
expect(
screen.getByText(`Session: ${mockSession.id}`),
).toBeInTheDocument(),
)

// Verify that the session data is displayed
expect(screen.getByText(`Session: ${mockSession.id}`)).toBeInTheDocument()
})

it("doesn't refetch session if a session is set", async () => {
;(frontendClient as jest.Mock).mockReturnValue({
toSession: jest.fn().mockResolvedValue(mockSession),
})

render(<TestComponent />)

// Initially, it should show loading
expect(screen.getByText("Loading...")).toBeInTheDocument()

// Wait for the hook to update
await waitFor(() =>
expect(
screen.getByText(`Session: ${mockSession.id}`),
).toBeInTheDocument(),
)

// Verify that the session data is displayed
expect(screen.getByText(`Session: ${mockSession.id}`)).toBeInTheDocument()

// this is fine, because jest is not calling the function
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(frontendClient(mockConfig.sdk.url).toSession).toHaveBeenCalledTimes(
1,
)

act(() => {
render(<TestComponent />)
})

// this is fine, because jest is not calling the function
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(frontendClient(mockConfig.sdk.url).toSession).toHaveBeenCalledTimes(
1,
)
})

it("handles errors during session fetching", async () => {
const errorMessage = "Failed to fetch session"
;(frontendClient as jest.Mock).mockReturnValue({
toSession: jest.fn().mockRejectedValue(new Error(errorMessage)),
})

render(<TestComponent />)

// Initially, it should show loading
expect(screen.getByText("Loading...")).toBeInTheDocument()

// Wait for the hook to update after the error
await waitFor(() =>
expect(screen.getByText(`Error: ${errorMessage}`)).toBeInTheDocument(),
)

// Verify that the error message is displayed
expect(screen.getByText(`Error: ${errorMessage}`)).toBeInTheDocument()
})

it("does not fetch session if already loading or session is set", async () => {
;(frontendClient as jest.Mock).mockReturnValue({
toSession: jest.fn(),
})

// First render: no session, simulate loading
render(<TestComponent />)

// Initially, it should show loading
expect(screen.getByText("Loading...")).toBeInTheDocument()

// Simulate session already being set in the store
await waitFor(() =>
expect(screen.getByText("No session")).toBeInTheDocument(),
)

// this is fine, because jest is not calling the function
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(frontendClient(mockConfig.sdk.url).toSession).toHaveBeenCalledTimes(
1,
)
})
})
74 changes: 74 additions & 0 deletions packages/elements-react/src/hooks/useSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { Session } from "@ory/client-fetch"
import { useCallback, useEffect } from "react"
import { create, useStore } from "zustand"
import { subscribeWithSelector } from "zustand/middleware"
import { useOryFlow } from "../context/flow-context"
import { frontendClient } from "../util/client"

type SessionStore = {
setIsLoading: (loading: boolean) => void
setSession: (session: Session) => void
isLoading: boolean
session: Session | undefined
error: string | undefined
setError: (error: string | undefined) => void
}

export const sessionStore = create<SessionStore>()(
subscribeWithSelector((set) => ({
isLoading: false,
setIsLoading: (isLoading: boolean) => set({ isLoading }),
session: undefined,
setSession: (session: Session) => set({ session }),
error: undefined,
setError: (error: string | undefined) => set({ error }),
})),
)

/**
* A hook to get the current session from the Ory Network.
*
* Usage:
* ```ts
* const { session, error, isLoading } = useSession()
* ```
*
* @returns The current session, error and loading state.
*/
export const useSession = () => {
const { config } = useOryFlow()
const store = useStore(sessionStore)

const fetchSession = useCallback(async () => {
const { session, isLoading, setSession, setIsLoading, setError } =
sessionStore.getState()

if (!!session || isLoading) {
return
}

setIsLoading(true)

try {
const sessionData = await frontendClient(config.sdk.url).toSession()
setSession(sessionData)
} catch (e) {
setError(e instanceof Error ? e.message : "Unknown error occurred")
} finally {
setIsLoading(false)
}
}, [config.sdk.url])

useEffect(() => {
void fetchSession()
}, [fetchSession])

return {
session: store.session,
error: store.error,
isLoading: store.isLoading,
}
}
1 change: 1 addition & 0 deletions packages/elements-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export type * from "./types"
export * from "./components"
export * from "./context"
export * from "./util"
export * from "./hooks"
export { locales as OryLocales } from "./locales"

0 comments on commit 06d876f

Please sign in to comment.