@sanity/sdk-react 0.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/_chunks-es/useLogOut.js +36 -0
  2. package/dist/_chunks-es/useLogOut.js.map +1 -0
  3. package/dist/components.d.ts +235 -0
  4. package/dist/components.js +250 -0
  5. package/dist/components.js.map +1 -0
  6. package/dist/hooks.d.ts +145 -0
  7. package/dist/hooks.js +27 -0
  8. package/dist/hooks.js.map +1 -0
  9. package/dist/index.d.ts +7 -0
  10. package/dist/index.js +6 -0
  11. package/dist/index.js.map +1 -0
  12. package/package.json +113 -0
  13. package/src/_exports/components.ts +12 -0
  14. package/src/_exports/hooks.ts +7 -0
  15. package/src/_exports/index.ts +10 -0
  16. package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +95 -0
  17. package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +42 -0
  18. package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +23 -0
  19. package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +95 -0
  20. package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +42 -0
  21. package/src/components/DocumentListLayout/DocumentListLayout.tsx +15 -0
  22. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +49 -0
  23. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +34 -0
  24. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +30 -0
  25. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +115 -0
  26. package/src/components/Login/LoginLinks.test.tsx +100 -0
  27. package/src/components/Login/LoginLinks.tsx +73 -0
  28. package/src/components/auth/AuthBoundary.test.tsx +103 -0
  29. package/src/components/auth/AuthBoundary.tsx +101 -0
  30. package/src/components/auth/AuthError.test.ts +36 -0
  31. package/src/components/auth/AuthError.ts +27 -0
  32. package/src/components/auth/Login.test.tsx +41 -0
  33. package/src/components/auth/Login.tsx +58 -0
  34. package/src/components/auth/LoginCallback.test.tsx +86 -0
  35. package/src/components/auth/LoginCallback.tsx +41 -0
  36. package/src/components/auth/LoginError.test.tsx +56 -0
  37. package/src/components/auth/LoginError.tsx +54 -0
  38. package/src/components/auth/LoginFooter.test.tsx +29 -0
  39. package/src/components/auth/LoginFooter.tsx +67 -0
  40. package/src/components/auth/LoginLayout.test.tsx +33 -0
  41. package/src/components/auth/LoginLayout.tsx +99 -0
  42. package/src/components/context/SanityProvider.test.tsx +25 -0
  43. package/src/components/context/SanityProvider.tsx +42 -0
  44. package/src/hooks/Documents/.keep +0 -0
  45. package/src/hooks/auth/useAuthState.test.tsx +106 -0
  46. package/src/hooks/auth/useAuthState.tsx +33 -0
  47. package/src/hooks/auth/useAuthToken.test.tsx +94 -0
  48. package/src/hooks/auth/useAuthToken.tsx +16 -0
  49. package/src/hooks/auth/useCurrentUser.test.tsx +50 -0
  50. package/src/hooks/auth/useCurrentUser.tsx +27 -0
  51. package/src/hooks/auth/useHandleCallback.test.tsx +25 -0
  52. package/src/hooks/auth/useHandleCallback.tsx +50 -0
  53. package/src/hooks/auth/useLogOut.test.tsx +67 -0
  54. package/src/hooks/auth/useLogOut.tsx +15 -0
  55. package/src/hooks/auth/useLoginUrls.test.tsx +61 -0
  56. package/src/hooks/auth/useLoginUrls.tsx +51 -0
  57. package/src/hooks/client/useClient.test.tsx +130 -0
  58. package/src/hooks/client/useClient.ts +56 -0
  59. package/src/hooks/context/useSanityInstance.test.tsx +31 -0
  60. package/src/hooks/context/useSanityInstance.ts +23 -0
@@ -0,0 +1,106 @@
1
+ import {
2
+ type AuthState,
3
+ type AuthStore,
4
+ createSanityInstance,
5
+ getAuthStore,
6
+ type SanityInstance,
7
+ } from '@sanity/sdk'
8
+ import {renderHook} from '@testing-library/react'
9
+ import {describe, expect, it, vi} from 'vitest'
10
+
11
+ import {SanityProvider} from '../../components/context/SanityProvider'
12
+ import * as context from '../context/useSanityInstance'
13
+ import {useAuthState} from './useAuthState'
14
+
15
+ // Mock dependencies
16
+ vi.mock('@sanity/sdk')
17
+ vi.mock('../context/useSanityInstance')
18
+
19
+ const createMockAuthStore = (authState: AuthState): AuthStore => ({
20
+ authState: {
21
+ getState: () => authState,
22
+ getInitialState: () => authState,
23
+ subscribe: vi.fn(),
24
+ },
25
+ tokenState: {
26
+ getState: vi.fn(),
27
+ getInitialState: vi.fn(),
28
+ subscribe: vi.fn(),
29
+ },
30
+ currentUserState: {
31
+ getState: vi.fn(),
32
+ getInitialState: vi.fn(),
33
+ subscribe: vi.fn(),
34
+ },
35
+ handleCallback: vi.fn(),
36
+ logout: vi.fn(),
37
+ dispose: vi.fn(),
38
+ getLoginUrls: vi.fn(),
39
+ })
40
+
41
+ const mockUser = {
42
+ id: 'user-123',
43
+ name: 'Test User',
44
+ email: 'test@example.com',
45
+ role: 'developer',
46
+ roles: [{name: 'developer', title: 'Developer'}],
47
+ }
48
+
49
+ describe('useAuthState', () => {
50
+ const mockInstance: SanityInstance = {
51
+ identity: {
52
+ id: 'abc123store',
53
+ projectId: 'project-123',
54
+ dataset: 'dataset-123',
55
+ },
56
+ config: {},
57
+ }
58
+
59
+ // Setup mock for useSanityInstance
60
+ beforeEach(() => {
61
+ vi.spyOn(context, 'useSanityInstance').mockReturnValue(mockInstance)
62
+ })
63
+
64
+ it('should return the current auth state', () => {
65
+ const mockAuthStore = createMockAuthStore({
66
+ type: 'logged-in',
67
+ token: 'token-123',
68
+ currentUser: mockUser,
69
+ })
70
+ vi.mocked(getAuthStore).mockReturnValue(mockAuthStore)
71
+
72
+ const sanityInstance = createSanityInstance({projectId: 'test', dataset: 'test'})
73
+ const {result} = renderHook(() => useAuthState(), {
74
+ wrapper: ({children}) => (
75
+ <SanityProvider sanityInstance={sanityInstance}>{children}</SanityProvider>
76
+ ),
77
+ })
78
+ const current = result.current as Extract<AuthState, {type: 'logged-in'}>
79
+ expect(current.type).toBe('logged-in')
80
+ expect(current.token).toBe('token-123')
81
+ expect(current.currentUser).toBe(mockUser)
82
+ })
83
+
84
+ it('should handle signed out state', () => {
85
+ const mockAuthStore = createMockAuthStore({type: 'logged-out', isDestroyingSession: false})
86
+ vi.mocked(getAuthStore).mockReturnValue(mockAuthStore)
87
+
88
+ const {result} = renderHook(() => useAuthState())
89
+ expect(result.current.type).toBe('logged-out')
90
+ })
91
+
92
+ it('should subscribe to auth state changes', () => {
93
+ const subscribe = vi.fn()
94
+ const mockAuthStore = createMockAuthStore({
95
+ type: 'logged-in',
96
+ token: 'token-123',
97
+ currentUser: null,
98
+ })
99
+ mockAuthStore.authState.subscribe = subscribe
100
+
101
+ vi.mocked(getAuthStore).mockReturnValue(mockAuthStore)
102
+
103
+ renderHook(() => useAuthState())
104
+ expect(subscribe).toHaveBeenCalled()
105
+ })
106
+ })
@@ -0,0 +1,33 @@
1
+ import {type AuthState, getAuthStore} from '@sanity/sdk'
2
+ import {useStore} from 'zustand/react'
3
+
4
+ import {useSanityInstance} from '../context/useSanityInstance'
5
+
6
+ /**
7
+ * A React hook that subscribes to authentication state changes.
8
+ *
9
+ * This hook provides access to the current authentication state type from the Sanity auth store.
10
+ * It automatically re-renders the component when the authentication state changes.
11
+ *
12
+ * @remarks
13
+ * The hook uses `useSyncExternalStore` to safely subscribe to auth state changes
14
+ * and ensure consistency between server and client rendering.
15
+ *
16
+ * @returns The current authentication state type
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * function AuthStatus() {
21
+ * const authState = useAuthState()
22
+ * return <div>Current auth state: {authState}</div>
23
+ * }
24
+ * ```
25
+ *
26
+ * @public
27
+ */
28
+ export function useAuthState(): AuthState {
29
+ const instance = useSanityInstance()
30
+ const {authState} = getAuthStore(instance)
31
+
32
+ return useStore(authState)
33
+ }
@@ -0,0 +1,94 @@
1
+ import {type AuthStore, getAuthStore, type SanityInstance} from '@sanity/sdk'
2
+ import {renderHook} from '@testing-library/react'
3
+ import {describe, expect, it, vi} from 'vitest'
4
+
5
+ import {useSanityInstance} from '../context/useSanityInstance'
6
+ import {useAuthToken} from './useAuthToken'
7
+
8
+ // Mock dependencies
9
+ vi.mock('@sanity/sdk', () => ({
10
+ getAuthStore: vi.fn(),
11
+ }))
12
+
13
+ vi.mock('../context/useSanityInstance', () => ({
14
+ useSanityInstance: vi.fn(),
15
+ }))
16
+
17
+ describe('useAuthToken', () => {
18
+ // Helper function to create mock instance
19
+ const createMockInstance = (): SanityInstance => ({
20
+ identity: {
21
+ id: 'abc123',
22
+ projectId: 'test-project-id',
23
+ dataset: 'test-dataset',
24
+ },
25
+ config: {},
26
+ })
27
+
28
+ // Helper function to create mock auth store
29
+ const createMockAuthStore = (token: string | null) => {
30
+ const authType = token ? 'logged-in' : 'logged-out'
31
+ return {
32
+ tokenState: {
33
+ getInitialState: () => token,
34
+ getState: () => token,
35
+ subscribe: vi.fn(),
36
+ },
37
+ authState: {
38
+ getInitialState: () => ({
39
+ type: authType,
40
+ isDestroyingSession: false,
41
+ ...(token && {token, currentUser: null}),
42
+ }),
43
+ getState: () => ({
44
+ type: authType,
45
+ isDestroyingSession: false,
46
+ ...(token && {token, currentUser: null}),
47
+ }),
48
+ subscribe: vi.fn(),
49
+ },
50
+ currentUserState: {
51
+ getInitialState: () => null,
52
+ getState: () => null,
53
+ subscribe: vi.fn(),
54
+ },
55
+ handleCallback: vi.fn(),
56
+ logout: vi.fn(),
57
+ dispose: vi.fn(),
58
+ getLoginUrls: vi.fn(),
59
+ } as unknown as AuthStore
60
+ }
61
+
62
+ beforeEach(() => {
63
+ vi.clearAllMocks()
64
+ })
65
+
66
+ it('should return null when no token is present', () => {
67
+ const mockInstance = createMockInstance()
68
+ const mockAuthStore = createMockAuthStore(null)
69
+
70
+ vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
71
+ vi.mocked(getAuthStore).mockReturnValue(mockAuthStore)
72
+
73
+ const {result} = renderHook(() => useAuthToken())
74
+
75
+ expect(result.current).toBeNull()
76
+ expect(useSanityInstance).toHaveBeenCalled()
77
+ expect(getAuthStore).toHaveBeenCalledWith(mockInstance)
78
+ })
79
+
80
+ it('should return token when authenticated', () => {
81
+ const mockInstance = createMockInstance()
82
+ const mockToken = 'test-auth-token'
83
+ const mockAuthStore = createMockAuthStore(mockToken)
84
+
85
+ vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
86
+ vi.mocked(getAuthStore).mockReturnValue(mockAuthStore)
87
+
88
+ const {result} = renderHook(() => useAuthToken())
89
+
90
+ expect(result.current).toBe(mockToken)
91
+ expect(useSanityInstance).toHaveBeenCalled()
92
+ expect(getAuthStore).toHaveBeenCalledWith(mockInstance)
93
+ })
94
+ })
@@ -0,0 +1,16 @@
1
+ import {getAuthStore} from '@sanity/sdk'
2
+ import {useStore} from 'zustand'
3
+
4
+ import {useSanityInstance} from '../context/useSanityInstance'
5
+
6
+ /**
7
+ * Hook to get the currently logged in user
8
+ * @public
9
+ * @returns The current user or null if not authenticated
10
+ */
11
+ export const useAuthToken = (): string | null => {
12
+ const instance = useSanityInstance()
13
+ const {tokenState} = getAuthStore(instance)
14
+
15
+ return useStore(tokenState)
16
+ }
@@ -0,0 +1,50 @@
1
+ import {type AuthStore, type CurrentUser, getAuthStore, type SanityInstance} from '@sanity/sdk'
2
+ import {renderHook} from '@testing-library/react'
3
+ import {describe, expect, it, vi} from 'vitest'
4
+
5
+ import {useSanityInstance} from '../context/useSanityInstance'
6
+ import {useCurrentUser} from './useCurrentUser'
7
+
8
+ // Mock dependencies
9
+ vi.mock('../context/useSanityInstance')
10
+ vi.mock('@sanity/sdk')
11
+
12
+ const mockUser: CurrentUser = {
13
+ id: 'user-123',
14
+ name: 'Test User',
15
+ email: 'test@example.com',
16
+ role: 'admin',
17
+ roles: [],
18
+ }
19
+
20
+ describe('useCurrentUser', () => {
21
+ it('returns the current user when authenticated', () => {
22
+ vi.mocked(useSanityInstance).mockReturnValue({} as unknown as SanityInstance)
23
+
24
+ // Mock the auth store with an authenticated user
25
+ vi.mocked(getAuthStore).mockReturnValue({
26
+ currentUserState: {
27
+ getState: () => mockUser,
28
+ subscribe: vi.fn(),
29
+ },
30
+ } as unknown as AuthStore)
31
+
32
+ const {result} = renderHook(() => useCurrentUser())
33
+ expect(result.current).toEqual(mockUser)
34
+ })
35
+
36
+ it('returns null when not authenticated', () => {
37
+ vi.mocked(useSanityInstance).mockReturnValue({} as unknown as SanityInstance)
38
+
39
+ // Mock the auth store with no user
40
+ vi.mocked(getAuthStore).mockReturnValue({
41
+ currentUserState: {
42
+ getState: () => null,
43
+ subscribe: vi.fn(),
44
+ },
45
+ } as unknown as AuthStore)
46
+
47
+ const {result} = renderHook(() => useCurrentUser())
48
+ expect(result.current).toBeNull()
49
+ })
50
+ })
@@ -0,0 +1,27 @@
1
+ import {type CurrentUser, type CurrentUserSlice, getAuthStore} from '@sanity/sdk'
2
+ import {useStore} from 'zustand'
3
+
4
+ import {useSanityInstance} from '../context/useSanityInstance'
5
+
6
+ /**
7
+ * Hook to get the currently logged in user
8
+ * @public
9
+ * @returns The current user or null if not authenticated
10
+ */
11
+ export const useCurrentUser = (): CurrentUser | null => {
12
+ const instance = useSanityInstance()
13
+ const {currentUserState} = getAuthStore(instance)
14
+
15
+ // TODO: update this hook so it can never return null
16
+ if (!currentUserState.getState())
17
+ throw new Promise<void>((resolve) => {
18
+ const unsubscribe = currentUserState.subscribe((currentUser) => {
19
+ if (currentUser) {
20
+ unsubscribe()
21
+ resolve()
22
+ }
23
+ })
24
+ })
25
+
26
+ return useStore<CurrentUserSlice>(currentUserState)
27
+ }
@@ -0,0 +1,25 @@
1
+ import {getAuthStore} from '@sanity/sdk'
2
+ import {renderHook} from '@testing-library/react'
3
+ import {type Mock, vi} from 'vitest'
4
+
5
+ import {useSanityInstance} from '../context/useSanityInstance'
6
+ import {useHandleCallback} from './useHandleCallback'
7
+
8
+ vi.mock('../context/useSanityInstance')
9
+ vi.mock('@sanity/sdk', () => ({getAuthStore: vi.fn()}))
10
+
11
+ describe('useHandleCallback', () => {
12
+ it('returns handleCallback from auth store', () => {
13
+ const mockInstance = {id: 'test'}
14
+ const mockHandleCallback = vi.fn()
15
+ const mockAuthStore = {handleCallback: mockHandleCallback}
16
+
17
+ ;(useSanityInstance as Mock).mockReturnValue(mockInstance)
18
+ ;(getAuthStore as Mock).mockReturnValue(mockAuthStore)
19
+
20
+ const {result} = renderHook(() => useHandleCallback())
21
+
22
+ expect(getAuthStore).toHaveBeenCalledWith(mockInstance)
23
+ expect(result.current).toBe(mockHandleCallback)
24
+ })
25
+ })
@@ -0,0 +1,50 @@
1
+ import {type AuthStore, getAuthStore} from '@sanity/sdk'
2
+ import {useMemo} from 'react'
3
+
4
+ import {useSanityInstance} from '../context/useSanityInstance'
5
+
6
+ /**
7
+ * A React hook that returns a function for handling authentication callbacks.
8
+ *
9
+ * @remarks
10
+ * This hook provides access to the authentication store's callback handler,
11
+ * which processes auth redirects by extracting the session ID and fetching the
12
+ * authentication token. If fetching the long-lived token is successful,
13
+ * `handleCallback` will return a Promise that resolves a new location that
14
+ * removes the short-lived token from the URL. Use this in combination with
15
+ * `history.replaceState` or your own router's `replace` function to update the
16
+ * current location without triggering a reload.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * function AuthCallback() {
21
+ * const handleCallback = useHandleCallback()
22
+ * const router = useRouter() // Example router
23
+ *
24
+ * useEffect(() => {
25
+ * async function processCallback() {
26
+ * // Handle the callback and get the cleaned URL
27
+ * const newUrl = await handleCallback(window.location.href)
28
+ *
29
+ * if (newUrl) {
30
+ * // Replace URL without triggering navigation
31
+ * router.replace(newUrl, {shallow: true})
32
+ * }
33
+ * }
34
+ *
35
+ * processCallback().catch(console.error)
36
+ * }, [handleCallback, router])
37
+ *
38
+ * return <div>Completing login...</div>
39
+ * }
40
+ * ```
41
+ *
42
+ * @returns A callback handler function that processes OAuth redirects
43
+ * @public
44
+ */
45
+ export function useHandleCallback(): AuthStore['handleCallback'] {
46
+ const instance = useSanityInstance()
47
+ const authStore = useMemo(() => getAuthStore(instance), [instance])
48
+
49
+ return authStore.handleCallback
50
+ }
@@ -0,0 +1,67 @@
1
+ import {type AuthStore, getAuthStore, type SanityInstance} from '@sanity/sdk'
2
+ import {renderHook} from '@testing-library/react'
3
+ import {describe, expect, it, vi} from 'vitest'
4
+
5
+ import {useSanityInstance} from '../context/useSanityInstance'
6
+ import {useLogOut} from './useLogOut'
7
+
8
+ // Mock dependencies
9
+ vi.mock('@sanity/sdk', () => ({
10
+ getAuthStore: vi.fn(),
11
+ }))
12
+
13
+ vi.mock('../context/useSanityInstance', () => ({
14
+ useSanityInstance: vi.fn(),
15
+ }))
16
+
17
+ describe('useLogOut', () => {
18
+ it('should return logout function from auth store', () => {
19
+ // Setup mocks
20
+ const mockInstance: SanityInstance = {
21
+ identity: {
22
+ id: 'abc123',
23
+ projectId: 'test-project-id',
24
+ dataset: 'test-dataset',
25
+ },
26
+ config: {},
27
+ }
28
+
29
+ const mockLogout = vi.fn()
30
+ const mockAuthStore: AuthStore = {
31
+ authState: {
32
+ getInitialState: vi.fn(),
33
+ getState: vi.fn(),
34
+ subscribe: vi.fn(),
35
+ },
36
+ tokenState: {
37
+ getInitialState: vi.fn(),
38
+ getState: vi.fn(),
39
+ subscribe: vi.fn(),
40
+ },
41
+ currentUserState: {
42
+ getInitialState: vi.fn(),
43
+ getState: vi.fn(),
44
+ subscribe: vi.fn(),
45
+ },
46
+ handleCallback: vi.fn(),
47
+ logout: mockLogout,
48
+ dispose: vi.fn(),
49
+ getLoginUrls: vi.fn(),
50
+ }
51
+
52
+ vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
53
+ vi.mocked(getAuthStore).mockReturnValue(mockAuthStore)
54
+
55
+ // Test the hook
56
+ const {result} = renderHook(() => useLogOut())
57
+
58
+ // Verify the returned function is the logout function
59
+ expect(result.current).toBe(mockLogout)
60
+ expect(useSanityInstance).toHaveBeenCalled()
61
+ expect(getAuthStore).toHaveBeenCalledWith(mockInstance)
62
+
63
+ // Verify the logout function can be called
64
+ result.current()
65
+ expect(mockLogout).toHaveBeenCalled()
66
+ })
67
+ })
@@ -0,0 +1,15 @@
1
+ import {type AuthStore, getAuthStore} from '@sanity/sdk'
2
+
3
+ import {useSanityInstance} from '../context/useSanityInstance'
4
+
5
+ /**
6
+ * Hook to log out of the current session
7
+ * @public
8
+ * @returns A function to log out of the current session
9
+ */
10
+ export const useLogOut = (): AuthStore['logout'] => {
11
+ const instance = useSanityInstance()
12
+ const {logout} = getAuthStore(instance)
13
+
14
+ return logout
15
+ }
@@ -0,0 +1,61 @@
1
+ import {type AuthStore, createSanityInstance, getAuthStore} from '@sanity/sdk'
2
+ import {renderHook, waitFor} from '@testing-library/react'
3
+ import {Suspense} from 'react'
4
+ import {describe, expect, it, vi} from 'vitest'
5
+
6
+ import {SanityProvider} from '../../components/context/SanityProvider'
7
+ import {useLoginUrls} from './useLoginUrls'
8
+
9
+ vi.mock(import('@sanity/sdk'), async (importOriginal) => {
10
+ const actual = await importOriginal()
11
+ return {
12
+ ...actual,
13
+ getAuthStore: vi.fn(),
14
+ }
15
+ })
16
+
17
+ describe('useLoginUrls', () => {
18
+ it('should handle synchronous provider URLs', () => {
19
+ const mockProviders = [{name: 'google', title: 'Google', url: 'http://test.com/auth/google'}]
20
+ const mockAuthStore = {
21
+ getLoginUrls: () => mockProviders,
22
+ }
23
+ vi.mocked(getAuthStore).mockReturnValue(mockAuthStore as AuthStore)
24
+
25
+ const sanityInstance = createSanityInstance({projectId: 'test', dataset: 'test'})
26
+ const wrapper = ({children}: {children: React.ReactNode}) => (
27
+ <SanityProvider sanityInstance={sanityInstance}>{children}</SanityProvider>
28
+ )
29
+
30
+ const {result} = renderHook(() => useLoginUrls(), {wrapper})
31
+ expect(result.current).toEqual(mockProviders)
32
+ })
33
+
34
+ it('should handle asynchronous provider URLs', async () => {
35
+ const mockProviders = [{name: 'google', title: 'Google', url: 'http://test.com/auth/google'}]
36
+
37
+ const getLoginUrls = vi
38
+ .fn<AuthStore['getLoginUrls']>()
39
+ .mockResolvedValueOnce(mockProviders)
40
+ .mockReturnValueOnce(mockProviders)
41
+
42
+ vi.mocked(getAuthStore).mockReturnValue({getLoginUrls} as unknown as AuthStore)
43
+
44
+ const sanityInstance = createSanityInstance({projectId: 'test', dataset: 'test'})
45
+ const wrapper = ({children}: {children: React.ReactNode}) => (
46
+ <Suspense fallback={<>Loading…</>}>
47
+ <SanityProvider sanityInstance={sanityInstance}>{children}</SanityProvider>
48
+ </Suspense>
49
+ )
50
+
51
+ const {result} = renderHook(() => useLoginUrls(), {wrapper})
52
+
53
+ // Initially empty
54
+ expect(result.current).toEqual(null)
55
+
56
+ // Wait for providers to load
57
+ await waitFor(() => {
58
+ expect(result.current).toEqual(mockProviders)
59
+ })
60
+ })
61
+ })
@@ -0,0 +1,51 @@
1
+ import {type AuthProvider, getAuthStore} from '@sanity/sdk'
2
+ import {useMemo} from 'react'
3
+
4
+ import {useSanityInstance} from '../context/useSanityInstance'
5
+
6
+ /**
7
+ * A React hook that retrieves the available authentication provider URLs for login.
8
+ *
9
+ * @remarks
10
+ * This hook fetches the login URLs from the Sanity auth store when the component mounts.
11
+ * Each provider object contains information about an authentication method, including its URL.
12
+ * The hook will suspend if the login URLs have not yet loaded.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * // LoginProviders component that uses the hook
17
+ * function LoginProviders() {
18
+ * const providers = useLoginUrls()
19
+ *
20
+ * return (
21
+ * <div>
22
+ * {providers.map((provider) => (
23
+ * <a key={provider.name} href={provider.url}>
24
+ * Login with {provider.title}
25
+ * </a>
26
+ * ))}
27
+ * </div>
28
+ * )
29
+ * }
30
+ *
31
+ * // Parent component with Suspense boundary
32
+ * function LoginPage() {
33
+ * return (
34
+ * <Suspense fallback={<div>Loading authentication providers...</div>}>
35
+ * <LoginProviders />
36
+ * </Suspense>
37
+ * )
38
+ * }
39
+ * ```
40
+ *
41
+ * @returns An array of {@link AuthProvider} objects containing login URLs and provider information
42
+ * @public
43
+ */
44
+ export function useLoginUrls(): AuthProvider[] {
45
+ const instance = useSanityInstance()
46
+ const authStore = useMemo(() => getAuthStore(instance), [instance])
47
+ const result = authStore.getLoginUrls()
48
+
49
+ if (Array.isArray(result)) return result
50
+ throw result
51
+ }