diff --git a/docs/data/toolpad/core/components/UserOrg.js b/docs/data/toolpad/core/components/UserOrg.js new file mode 100644 index 00000000000..0ed7253564c --- /dev/null +++ b/docs/data/toolpad/core/components/UserOrg.js @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { Box, Stack, Typography, Avatar, Link, Divider } from '@mui/material'; +import { + AccountPreview, + AccountPopoverFooter, + SignOutButton, +} from '@toolpad/core/Account'; + +import { useSession } from '@toolpad/core/useSession'; + +export function UserOrg() { + const session = useSession(); + if (!session?.user) { + return No user session available; + } + + const { logo: orgLogo, name: orgName, url: orgUrl } = session.org; + + return ( + + + {session.org && ( + + + This account is managed by + + + + + + {orgName} + + + {orgUrl} + + + + + )} + + + + + + ); +} diff --git a/docs/data/toolpad/core/components/UserOrg.tsx b/docs/data/toolpad/core/components/UserOrg.tsx new file mode 100644 index 00000000000..637d3948f40 --- /dev/null +++ b/docs/data/toolpad/core/components/UserOrg.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { Box, Stack, Typography, Avatar, Link, Divider } from '@mui/material'; +import { + AccountPreview, + AccountPopoverFooter, + SignOutButton, +} from '@toolpad/core/Account'; +import { Session } from '@toolpad/core/AppProvider'; +import { useSession } from '@toolpad/core/useSession'; + +export interface CustomSession extends Session { + org: { + name: string; + url: string; + logo: string; + }; +} + +export function UserOrg() { + const session = useSession(); + if (!session?.user) { + return No user session available; + } + + const { logo: orgLogo, name: orgName, url: orgUrl } = session.org; + + return ( + + + {session.org && ( + + + This account is managed by + + + + + + {orgName} + + + {orgUrl} + + + + + )} + + + + + + ); +} diff --git a/docs/data/toolpad/core/components/account/AccountCustomUserDetails.js b/docs/data/toolpad/core/components/account/AccountCustomUserDetails.js new file mode 100644 index 00000000000..f9ebd5d6e1a --- /dev/null +++ b/docs/data/toolpad/core/components/account/AccountCustomUserDetails.js @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { Account } from '@toolpad/core/Account'; +import { AuthenticationContext, SessionContext } from '@toolpad/core/AppProvider'; +import { UserOrg } from '../UserOrg'; + +const demoSession = { + user: { + name: 'Bharat Kashyap', + email: 'bharat@mui.com', + image: 'https://avatars.githubusercontent.com/u/19550456', + }, + org: { + name: 'MUI Inc.', + url: 'https://mui.com', + logo: 'https://mui.com/static/logo.svg', + }, +}; + +export default function AccountCustomUserDetails() { + const [customSession, setCustomSession] = React.useState(demoSession); + const authentication = React.useMemo(() => { + return { + signIn: () => { + setCustomSession(demoSession); + }, + signOut: () => { + setCustomSession(null); + }, + }; + }, []); + + return ( + + + {/* preview-start */} + + {/* preview-end */} + + + ); +} diff --git a/docs/data/toolpad/core/components/account/AccountCustomUserDetails.tsx b/docs/data/toolpad/core/components/account/AccountCustomUserDetails.tsx new file mode 100644 index 00000000000..11aecc9f557 --- /dev/null +++ b/docs/data/toolpad/core/components/account/AccountCustomUserDetails.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { Account } from '@toolpad/core/Account'; +import { AuthenticationContext, SessionContext } from '@toolpad/core/AppProvider'; +import { UserOrg, CustomSession } from '../UserOrg'; + +const demoSession: CustomSession = { + user: { + name: 'Bharat Kashyap', + email: 'bharat@mui.com', + image: 'https://avatars.githubusercontent.com/u/19550456', + }, + org: { + name: 'MUI Inc.', + url: 'https://mui.com', + logo: 'https://mui.com/static/logo.svg', + }, +}; + +export default function AccountCustomUserDetails() { + const [customSession, setCustomSession] = React.useState( + demoSession, + ); + const authentication = React.useMemo(() => { + return { + signIn: () => { + setCustomSession(demoSession); + }, + signOut: () => { + setCustomSession(null); + }, + }; + }, []); + + return ( + + + {/* preview-start */} + + {/* preview-end */} + + + ); +} diff --git a/docs/data/toolpad/core/components/account/AccountCustomUserDetails.tsx.preview b/docs/data/toolpad/core/components/account/AccountCustomUserDetails.tsx.preview new file mode 100644 index 00000000000..d6f6e29f9bb --- /dev/null +++ b/docs/data/toolpad/core/components/account/AccountCustomUserDetails.tsx.preview @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/account/AccountLocale.tsx.preview b/docs/data/toolpad/core/components/account/AccountLocale.tsx.preview new file mode 100644 index 00000000000..78c0013b2a5 --- /dev/null +++ b/docs/data/toolpad/core/components/account/AccountLocale.tsx.preview @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/account/AccountSlotsWallet.js b/docs/data/toolpad/core/components/account/AccountSlotsWallet.js new file mode 100644 index 00000000000..44de238600e --- /dev/null +++ b/docs/data/toolpad/core/components/account/AccountSlotsWallet.js @@ -0,0 +1,126 @@ +import * as React from 'react'; +import { + Avatar, + Button, + Divider, + Typography, + Stack, + IconButton, +} from '@mui/material'; +import WalletIcon from '@mui/icons-material/AccountBalance'; +import SendIcon from '@mui/icons-material/Send'; +import ShoppingCart from '@mui/icons-material/ShoppingCart'; +import CopyIcon from '@mui/icons-material/ContentCopy'; + +import { + Account, + AccountPreview, + AccountPopoverFooter, + SignOutButton, +} from '@toolpad/core/Account'; + +import { AuthenticationContext, SessionContext } from '@toolpad/core/AppProvider'; + +const demoSession = { + user: { + name: 'Bharat Kashyap', + email: 'bharatkashyap@outlook.com', + image: 'https://avatars.githubusercontent.com/u/19550456', + }, +}; + +const mockData = { + address: '0xb794f5ea0ba39494ce839613fffba74279579268', + balance: '1,234.56 ETH', + usdBalance: '$2,345,678.90 USD', +}; + +function CryptoWalletInfo() { + return ( + + + + + + + + + + + + {mockData.address} + + + + + + + Main Account + + + + + + + {mockData.balance} + + + {mockData.usdBalance} + + + + + + + + + + + ); +} + +export default function AccountSlotsWallet() { + const [session, setSession] = React.useState(demoSession); + const authentication = React.useMemo(() => { + return { + signIn: () => { + setSession(demoSession); + }, + signOut: () => { + setSession(null); + }, + }; + }, []); + + return ( + + + + + + ); +} diff --git a/docs/data/toolpad/core/components/account/AccountSlotsWallet.tsx b/docs/data/toolpad/core/components/account/AccountSlotsWallet.tsx new file mode 100644 index 00000000000..4e7c63e82cb --- /dev/null +++ b/docs/data/toolpad/core/components/account/AccountSlotsWallet.tsx @@ -0,0 +1,130 @@ +import * as React from 'react'; +import { + Avatar, + Button, + Divider, + Typography, + Stack, + IconButton, +} from '@mui/material'; +import WalletIcon from '@mui/icons-material/AccountBalance'; +import SendIcon from '@mui/icons-material/Send'; +import ShoppingCart from '@mui/icons-material/ShoppingCart'; +import CopyIcon from '@mui/icons-material/ContentCopy'; + +import { + Account, + AccountPreview, + AccountPopoverFooter, + SignOutButton, +} from '@toolpad/core/Account'; + +import { + AuthenticationContext, + SessionContext, + Session, +} from '@toolpad/core/AppProvider'; + +const demoSession = { + user: { + name: 'Bharat Kashyap', + email: 'bharatkashyap@outlook.com', + image: 'https://avatars.githubusercontent.com/u/19550456', + }, +}; + +const mockData = { + address: '0xb794f5ea0ba39494ce839613fffba74279579268', + balance: '1,234.56 ETH', + usdBalance: '$2,345,678.90 USD', +}; + +function CryptoWalletInfo() { + return ( + + + + + + + + + + + + {mockData.address} + + + + + + + Main Account + + + + + + + {mockData.balance} + + + {mockData.usdBalance} + + + + + + + + + + + ); +} + +export default function AccountSlotsWallet() { + const [session, setSession] = React.useState(demoSession); + const authentication = React.useMemo(() => { + return { + signIn: () => { + setSession(demoSession); + }, + signOut: () => { + setSession(null); + }, + }; + }, []); + + return ( + + + + + + ); +} diff --git a/docs/data/toolpad/core/components/account/AccountSlotsWallet.tsx.preview b/docs/data/toolpad/core/components/account/AccountSlotsWallet.tsx.preview new file mode 100644 index 00000000000..7241ae7a264 --- /dev/null +++ b/docs/data/toolpad/core/components/account/AccountSlotsWallet.tsx.preview @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/docs/data/toolpad/core/components/account/account.md b/docs/data/toolpad/core/components/account/account.md index a706c176a50..90c84b27c6f 100644 --- a/docs/data/toolpad/core/components/account/account.md +++ b/docs/data/toolpad/core/components/account/account.md @@ -66,3 +66,11 @@ You can build advanced menus – such as a tenant switcher – by passing in a c You can pass in custom labels – including of different languages – using the `localeText` prop. {{"demo": "AccountCustomLocaleText.js", "bg": "outlined" }} + +### Session + +You can use the `useSession` hook to extend the existing session and add additional user details: + +{{"demo": "./AccountCustomUserDetails.js", "bg": "outlined", "defaultCodeOpen": false}} + +You can find more details on the [`useSession` docs page](/toolopad/core/react-use-session/). diff --git a/docs/data/toolpad/core/components/use-session/use-session-api.md b/docs/data/toolpad/core/components/use-session/use-session-api.md new file mode 100644 index 00000000000..24a5182b7b7 --- /dev/null +++ b/docs/data/toolpad/core/components/use-session/use-session-api.md @@ -0,0 +1,42 @@ +# useSession API + +

API reference for the useSession hook.

+ +:::success +For examples and details on the usage of this React hook, visit the demo pages: + +- [Account](/toolpad/core/react-account/) +- [useSession](/toolpad/core/react-use-Session/) + +::: + +## Import + +```js +import useSession from '@toolpad/core/useSession'; +// or +import { useSession } from '@toolpad/core'; +``` + +Learn about the difference by reading this [guide](https://mui.com/material-ui/guides/minimizing-bundle-size/) on minimizing bundle size. + +## Usage + +You can get access to the current value of the `SessionContext` inside Toolpad Core components by invoking the hook: + +```js +const session = useSession(); +``` + +The default `Session` interface exported by `@toolpad/core` has the following fields: + +```ts +export interface Session { + user?: { + id?: string | null; + name?: string | null; + image?: string | null; + email?: string | null; + }; +} +``` diff --git a/docs/data/toolpad/core/components/use-session/use-session.md b/docs/data/toolpad/core/components/use-session/use-session.md new file mode 100644 index 00000000000..c477ea77682 --- /dev/null +++ b/docs/data/toolpad/core/components/use-session/use-session.md @@ -0,0 +1,54 @@ +--- +productId: toolpad-core +title: useSession +--- + +# useSession + +

Toolpad Core exposes an API to access the current authentication session, regardless of the underlying authentication provider used.

+ +:::info +If this is your first time using Toolpad Core, it's recommended to read about the [basic concepts](/toolpad/core/introduction/base-concepts/) first. +::: + +## Usage + +When using authentication features inside Toolpad Core, a `SessionContext` is created to share session information among all Toolpad Core components. This accepts a default value from the `session` prop of the `AppProvider`. + +```js +{props.children} +``` + +The `session` can be created using any authentication provider of your choice. You can access the current value of the `SessionContext` inside Toolpad Core components by invoking the hook: + +```js +const session = useSession(); +``` + +If your session has additional data which you want to display in the account popover, you an create custom components for user information display with the session object: + +```ts +interface CustomSession { + org: { + name: string; + url: string; + logo: string; + }; +} + +function CustomAccountDetails() { + + const session = useSession(); + return ( + // Use `session.org` + ) +} +``` + +The following example demonstrates this behaviour clearly: + +{{"demo": "../account/AccountCustomUserDetails.js", "bg":"outlined"}} + +## Hook API + +- [`useSession()`](/toolpad/core/react-use-session/api/) diff --git a/docs/data/toolpad/core/pages.ts b/docs/data/toolpad/core/pages.ts index e0fb156d619..0a9e9f1d595 100644 --- a/docs/data/toolpad/core/pages.ts +++ b/docs/data/toolpad/core/pages.ts @@ -128,6 +128,10 @@ const pages: MuiPage[] = [ pathname: '/toolpad/core/react-use-notifications', title: 'useNotifications', }, + { + pathname: '/toolpad/core/react-use-session', + title: 'useSession', + }, { pathname: '/toolpad/core/react-persistent-state', title: 'Persisted state', @@ -161,6 +165,10 @@ const pages: MuiPage[] = [ pathname: '/toolpad/core/react-persistent-state/use-local-storage-state-api', title: 'useLocalStorageState', }, + { + pathname: '/toolpad/core/react-use-session/api', + title: 'useSession', + }, { pathname: '/toolpad/core/react-persistent-state/use-session-storage-state-api', title: 'useSessionStorageState', diff --git a/docs/pages/toolpad/core/react-use-session/api.js b/docs/pages/toolpad/core/react-use-session/api.js new file mode 100644 index 00000000000..d583ed0de3e --- /dev/null +++ b/docs/pages/toolpad/core/react-use-session/api.js @@ -0,0 +1,7 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import * as pageProps from 'docs-toolpad/data/toolpad/core/components/use-session/use-session-api.md?muiMarkdown'; + +export default function Page() { + return ; +} diff --git a/docs/pages/toolpad/core/react-use-session/index.js b/docs/pages/toolpad/core/react-use-session/index.js new file mode 100644 index 00000000000..b77a30a4fe5 --- /dev/null +++ b/docs/pages/toolpad/core/react-use-session/index.js @@ -0,0 +1,7 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import * as pageProps from 'docs-toolpad/data/toolpad/core/components/use-session/use-session.md?muiMarkdown'; + +export default function Page() { + return ; +} diff --git a/packages/toolpad-core/src/Account/Account.tsx b/packages/toolpad-core/src/Account/Account.tsx index b4aace2f797..0c6a105f5e1 100644 --- a/packages/toolpad-core/src/Account/Account.tsx +++ b/packages/toolpad-core/src/Account/Account.tsx @@ -60,6 +60,7 @@ export interface AccountProps { */ localeText?: Partial>; } + /** * * Demos: diff --git a/packages/toolpad-core/src/index.ts b/packages/toolpad-core/src/index.ts index dcb98090703..123c479d6c3 100644 --- a/packages/toolpad-core/src/index.ts +++ b/packages/toolpad-core/src/index.ts @@ -16,6 +16,8 @@ export * from './useNotifications'; export * from './useLocalStorageState'; +export * from './useSession'; + export * from './useSessionStorageState'; export * from './persistence/codec'; diff --git a/packages/toolpad-core/src/useSession/index.ts b/packages/toolpad-core/src/useSession/index.ts new file mode 100644 index 00000000000..c47a6987dd9 --- /dev/null +++ b/packages/toolpad-core/src/useSession/index.ts @@ -0,0 +1 @@ +export * from './useSession'; diff --git a/packages/toolpad-core/src/useSession/useSession.test.tsx b/packages/toolpad-core/src/useSession/useSession.test.tsx new file mode 100644 index 00000000000..d0add057a91 --- /dev/null +++ b/packages/toolpad-core/src/useSession/useSession.test.tsx @@ -0,0 +1,45 @@ +/** + * @vitest-environment jsdom + */ + +import * as React from 'react'; +import { renderHook } from '@testing-library/react'; +import { describe, test, expect } from 'vitest'; +import { useSession } from './useSession'; +import { Session, SessionContext } from '../AppProvider/AppProvider'; + +// Mock the session data +const mockSession = { + user: { + name: 'Bharat Kashyap', + email: 'bharat@mui.com', + image: 'https://avatars.githubusercontent.com/u/19550456', + }, +}; + +interface TestWrapperProps { + session: Session | null; + children: React.ReactNode; +} + +function TestWrapper({ children, session }: TestWrapperProps) { + return {children}; +} + +describe('useSession hook', () => { + test('should return session data when authenticated', () => { + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }) => {children}, + }); + + expect(result.current).toEqual(mockSession); + }); + + test('should return null session when not authenticated', () => { + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }) => {children}, + }); + + expect(result.current).toBeNull(); + }); +}); diff --git a/packages/toolpad-core/src/useSession/useSession.ts b/packages/toolpad-core/src/useSession/useSession.ts new file mode 100644 index 00000000000..41742b233c6 --- /dev/null +++ b/packages/toolpad-core/src/useSession/useSession.ts @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { SessionContext, Session } from '../AppProvider'; + +/** + * Hook to access the current Toolpad Core session. + * @returns The current session object or null if no session is available. + */ +export function useSession(): T | null { + const session = React.useContext(SessionContext); + return session as T | null; +}