@sanity/sdk-react 0.0.0-alpha.21 → 0.0.0-alpha.23

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 (71) hide show
  1. package/dist/index.d.ts +502 -3460
  2. package/dist/index.js +400 -465
  3. package/dist/index.js.map +1 -1
  4. package/package.json +17 -15
  5. package/src/_exports/index.ts +4 -5
  6. package/src/components/SDKProvider.test.tsx +78 -54
  7. package/src/components/SDKProvider.tsx +31 -26
  8. package/src/components/SanityApp.test.tsx +121 -15
  9. package/src/components/SanityApp.tsx +26 -15
  10. package/src/components/auth/AuthBoundary.test.tsx +32 -14
  11. package/src/components/auth/AuthBoundary.tsx +53 -23
  12. package/src/components/auth/LoginCallback.test.tsx +19 -6
  13. package/src/components/auth/LoginCallback.tsx +2 -11
  14. package/src/components/auth/LoginError.test.tsx +12 -4
  15. package/src/components/auth/LoginError.tsx +13 -21
  16. package/src/components/auth/LoginFooter.test.tsx +7 -3
  17. package/src/context/ResourceProvider.test.tsx +157 -0
  18. package/src/context/ResourceProvider.tsx +111 -0
  19. package/src/context/SanityInstanceContext.ts +1 -1
  20. package/src/hooks/auth/useLoginUrl.tsx +14 -0
  21. package/src/hooks/client/useClient.ts +2 -1
  22. package/src/hooks/comlink/useManageFavorite.test.ts +16 -8
  23. package/src/hooks/comlink/useManageFavorite.ts +37 -13
  24. package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +8 -4
  25. package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +10 -8
  26. package/src/hooks/context/useSanityInstance.test.tsx +157 -15
  27. package/src/hooks/context/useSanityInstance.ts +66 -26
  28. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +13 -31
  29. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +12 -15
  30. package/src/hooks/dashboard/{useStudioWorkspacesByResourceId.test.tsx → useStudioWorkspacesByProjectIdDataset.test.tsx} +13 -13
  31. package/src/hooks/dashboard/{useStudioWorkspacesByResourceId.ts → useStudioWorkspacesByProjectIdDataset.ts} +10 -9
  32. package/src/hooks/datasets/useDatasets.ts +15 -4
  33. package/src/hooks/document/useApplyDocumentActions.test.ts +4 -9
  34. package/src/hooks/document/useApplyDocumentActions.ts +6 -31
  35. package/src/hooks/document/useDocument.test.ts +2 -2
  36. package/src/hooks/document/useDocument.ts +40 -19
  37. package/src/hooks/document/useDocumentEvent.test.ts +2 -3
  38. package/src/hooks/document/useDocumentEvent.ts +7 -11
  39. package/src/hooks/document/useDocumentPermissions.test.ts +204 -0
  40. package/src/hooks/document/useDocumentPermissions.ts +31 -23
  41. package/src/hooks/document/useDocumentSyncStatus.ts +5 -4
  42. package/src/hooks/document/useEditDocument.test.ts +2 -3
  43. package/src/hooks/document/useEditDocument.ts +43 -29
  44. package/src/hooks/documents/useDocuments.test.tsx +30 -3
  45. package/src/hooks/documents/useDocuments.ts +20 -7
  46. package/src/hooks/helpers/createCallbackHook.test.tsx +2 -2
  47. package/src/hooks/helpers/createCallbackHook.tsx +2 -3
  48. package/src/hooks/helpers/createStateSourceHook.test.tsx +1 -1
  49. package/src/hooks/helpers/createStateSourceHook.tsx +5 -8
  50. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +43 -18
  51. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +36 -50
  52. package/src/hooks/preview/usePreview.test.tsx +66 -7
  53. package/src/hooks/preview/usePreview.tsx +17 -12
  54. package/src/hooks/projection/useProjection.test.tsx +68 -3
  55. package/src/hooks/projection/useProjection.ts +21 -24
  56. package/src/hooks/projects/useProject.ts +7 -4
  57. package/src/hooks/query/useQuery.ts +32 -14
  58. package/src/hooks/users/useUsers.test.tsx +330 -0
  59. package/src/hooks/users/useUsers.ts +65 -52
  60. package/src/components/Login/LoginLinks.test.tsx +0 -90
  61. package/src/components/Login/LoginLinks.tsx +0 -58
  62. package/src/components/auth/Login.test.tsx +0 -27
  63. package/src/components/auth/Login.tsx +0 -39
  64. package/src/components/auth/LoginLayout.test.tsx +0 -19
  65. package/src/components/auth/LoginLayout.tsx +0 -69
  66. package/src/components/auth/authTestHelpers.tsx +0 -11
  67. package/src/context/SanityProvider.test.tsx +0 -25
  68. package/src/context/SanityProvider.tsx +0 -50
  69. package/src/hooks/auth/useLoginUrls.test.tsx +0 -68
  70. package/src/hooks/auth/useLoginUrls.tsx +0 -52
  71. package/src/hooks/users/useUsers.test.ts +0 -163
@@ -1,5 +1,13 @@
1
- import {createUsersStore, type ResourceType, type SanityUser} from '@sanity/sdk'
2
- import {useCallback, useEffect, useState, useSyncExternalStore} from 'react'
1
+ import {
2
+ getUsersKey,
3
+ type GetUsersOptions,
4
+ getUsersState,
5
+ loadMoreUsers,
6
+ parseUsersKey,
7
+ resolveUsers,
8
+ type SanityUser,
9
+ } from '@sanity/sdk'
10
+ import {useCallback, useEffect, useMemo, useState, useSyncExternalStore, useTransition} from 'react'
3
11
 
4
12
  import {useSanityInstance} from '../context/useSanityInstance'
5
13
 
@@ -7,34 +15,20 @@ import {useSanityInstance} from '../context/useSanityInstance'
7
15
  * @public
8
16
  * @category Types
9
17
  */
10
- export interface UseUsersParams {
11
- /**
12
- * The type of resource to fetch users for.
13
- */
14
- resourceType: ResourceType
15
- /**
16
- * The ID of the resource to fetch users for.
17
- */
18
- resourceId: string
19
- /**
20
- * The limit of users to fetch.
21
- */
22
- limit?: number
23
- }
24
-
25
- /**
26
- * @public
27
- * @category Types
28
- */
29
- export interface UseUsersResult {
18
+ export interface UsersResult {
30
19
  /**
31
20
  * The users fetched.
32
21
  */
33
- users: SanityUser[]
22
+ data: SanityUser[]
34
23
  /**
35
24
  * Whether there are more users to fetch.
36
25
  */
37
26
  hasMore: boolean
27
+
28
+ /**
29
+ * Whether a users request is currently in progress
30
+ */
31
+ isPending: boolean
38
32
  /**
39
33
  * Load more users.
40
34
  */
@@ -48,60 +42,79 @@ export interface UseUsersResult {
48
42
  * Retrieves the users for a given resource (either a project or an organization).
49
43
  *
50
44
  * @category Users
51
- * @param params - The resource type and its ID, and the limit of users to fetch
45
+ * @param params - The resource type, project ID, and the limit of users to fetch
52
46
  * @returns A list of users, a boolean indicating whether there are more users to fetch, and a function to load more users
53
47
  *
54
48
  * @example
55
49
  * ```
56
- * const { users, hasMore, loadMore } = useUsers({
50
+ * const { data, hasMore, loadMore, isPending } = useUsers({
57
51
  * resourceType: 'organization',
58
- * resourceId: 'my-org-id',
59
- * limit: 10,
52
+ * organizationId: 'my-org-id',
53
+ * batchSize: 10,
60
54
  * })
61
55
  *
62
56
  * return (
63
57
  * <div>
64
- * {users.map(user => (
58
+ * {data.map(user => (
65
59
  * <figure key={user.sanityUserId}>
66
60
  * <img src={user.profile.imageUrl} alt='' />
67
61
  * <figcaption>{user.profile.displayName}</figcaption>
68
62
  * <address>{user.profile.email}</address>
69
63
  * </figure>
70
64
  * ))}
71
- * {hasMore && <button onClick={loadMore}>Load More</button>}
65
+ * {hasMore && <button onClick={loadMore}>{isPending ? 'Loading...' : 'Load More'</button>}
72
66
  * </div>
73
67
  * )
74
68
  * ```
75
69
  */
76
- export function useUsers(params: UseUsersParams): UseUsersResult {
77
- const instance = useSanityInstance(params.resourceId)
78
- const [store] = useState(() => createUsersStore(instance))
70
+ export function useUsers(options?: GetUsersOptions): UsersResult {
71
+ const instance = useSanityInstance(options)
72
+ // Use React's useTransition to avoid UI jank when user options change
73
+ const [isPending, startTransition] = useTransition()
74
+
75
+ // Get the unique key for this users request and its options
76
+ const key = getUsersKey(instance, options)
77
+ // Use a deferred state to avoid immediate re-renders when the users request changes
78
+ const [deferredKey, setDeferredKey] = useState(key)
79
+ // Parse the deferred users key back into users options
80
+ const deferred = useMemo(() => parseUsersKey(deferredKey), [deferredKey])
81
+
82
+ // Create an AbortController to cancel in-flight requests when needed
83
+ const [ref, setRef] = useState<AbortController>(new AbortController())
79
84
 
85
+ // When the users request or options change, start a transition to update the request
80
86
  useEffect(() => {
81
- store.setOptions({
82
- resourceType: params.resourceType,
83
- resourceId: params.resourceId,
84
- })
85
- }, [params.resourceType, params.resourceId, store])
87
+ if (key === deferredKey) return
86
88
 
87
- const subscribe = useCallback(
88
- (onStoreChanged: () => void) => {
89
- if (store.getState().getCurrent().initialFetchCompleted === false) {
90
- store.resolveUsers()
89
+ startTransition(() => {
90
+ if (!ref.signal.aborted) {
91
+ ref.abort()
92
+ setRef(new AbortController())
91
93
  }
92
- const unsubscribe = store.getState().subscribe(onStoreChanged)
93
94
 
94
- return () => {
95
- unsubscribe()
96
- store.dispose()
97
- }
98
- },
99
- [store],
100
- )
95
+ setDeferredKey(key)
96
+ })
97
+ }, [deferredKey, key, ref])
98
+
99
+ // Get the state source for this users request from the users store
100
+ const {getCurrent, subscribe} = useMemo(() => {
101
+ return getUsersState(instance, deferred)
102
+ }, [instance, deferred])
103
+
104
+ // If data isn't available yet, suspend rendering until it is
105
+ // This is the React Suspense integration - throwing a promise
106
+ // will cause React to show the nearest Suspense fallback
107
+ if (getCurrent() === undefined) {
108
+ throw resolveUsers(instance, {...deferred, signal: ref.signal})
109
+ }
101
110
 
102
- const getSnapshot = useCallback(() => store.getState().getCurrent(), [store])
111
+ // Subscribe to updates and get the current data
112
+ // useSyncExternalStore ensures the component re-renders when the data changes
113
+ const {data, hasMore} = useSyncExternalStore(subscribe, getCurrent)!
103
114
 
104
- const {users, hasMore} = useSyncExternalStore(subscribe, getSnapshot) || {}
115
+ const loadMore = useCallback(() => {
116
+ loadMoreUsers(instance, options)
117
+ }, [instance, options])
105
118
 
106
- return {users, hasMore, loadMore: store.loadMore}
119
+ return {data, hasMore, isPending, loadMore}
107
120
  }
@@ -1,90 +0,0 @@
1
- import {AuthStateType, createSanityInstance} from '@sanity/sdk'
2
- import {SanityProvider, useAuthState, useLoginUrls} from '@sanity/sdk-react'
3
- import {render, screen} from '@testing-library/react'
4
- import React from 'react'
5
- import {beforeEach, describe, expect, it, vi} from 'vitest'
6
-
7
- import {LoginLinks} from './LoginLinks'
8
-
9
- // Mock the hooks and SDK functions
10
- vi.mock('../../hooks/auth/useLoginUrls', () => ({
11
- useLoginUrls: vi.fn(() => [
12
- {
13
- name: 'google',
14
- title: 'Google',
15
- url: 'https://google.com/auth',
16
- },
17
- {
18
- name: 'github',
19
- title: 'GitHub',
20
- url: 'https://github.com/auth',
21
- },
22
- ]),
23
- }))
24
- vi.mock('@sanity/sdk', async () => {
25
- const actual = await vi.importActual('@sanity/sdk')
26
-
27
- return {
28
- ...actual,
29
- tradeTokenForSession: vi.fn(),
30
- getSidUrlHash: vi.fn().mockReturnValue(null),
31
- getSidUrlSearch: vi.fn(),
32
- }
33
- })
34
-
35
- vi.mock('../../hooks/auth/useAuthState', () => ({
36
- useAuthState: vi.fn(() => 'logged-out'),
37
- }))
38
-
39
- vi.mock('../../hooks/auth/useHandleAuthCallback', () => ({
40
- useHandleAuthCallback: vi.fn(),
41
- }))
42
-
43
- describe('LoginLinks', () => {
44
- beforeEach(() => {
45
- vi.clearAllMocks()
46
- })
47
-
48
- const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
49
- const renderWithWrappers = (ui: React.ReactElement) => {
50
- return render(<SanityProvider sanityInstances={[sanityInstance]}>{ui}</SanityProvider>)
51
- }
52
-
53
- it('renders auth provider links correctly when not authenticated', () => {
54
- vi.mocked(useAuthState).mockReturnValue({
55
- type: AuthStateType.LOGGED_OUT,
56
- isDestroyingSession: false,
57
- })
58
- renderWithWrappers(<LoginLinks />)
59
-
60
- expect(screen.getByText('Choose login provider')).toBeInTheDocument()
61
-
62
- const authProviders = useLoginUrls()
63
- authProviders.forEach((provider) => {
64
- const button = screen.getByRole('link', {name: provider.title})
65
- expect(button).toBeInTheDocument()
66
- expect(button).toHaveAttribute('href', provider.url)
67
- })
68
- })
69
-
70
- it('shows loading state while logging in', () => {
71
- vi.mocked(useAuthState).mockReturnValue({
72
- type: AuthStateType.LOGGING_IN,
73
- isExchangingToken: false,
74
- })
75
- renderWithWrappers(<LoginLinks />)
76
-
77
- expect(screen.getByText('Logging in...')).toBeInTheDocument()
78
- })
79
-
80
- it('shows success message when logged in', () => {
81
- vi.mocked(useAuthState).mockReturnValue({
82
- type: AuthStateType.LOGGED_IN,
83
- token: 'test-token',
84
- currentUser: null,
85
- })
86
- renderWithWrappers(<LoginLinks />)
87
-
88
- expect(screen.getByText('You are logged in')).toBeInTheDocument()
89
- })
90
- })
@@ -1,58 +0,0 @@
1
- import {type ReactElement} from 'react'
2
-
3
- import {useAuthState} from '../../hooks/auth/useAuthState'
4
- import {useHandleAuthCallback} from '../../hooks/auth/useHandleAuthCallback'
5
- import {useLoginUrls} from '../../hooks/auth/useLoginUrls'
6
-
7
- /**
8
- * Component that handles Sanity authentication flow and renders login provider options
9
- *
10
- * @public
11
- *
12
- * @returns Rendered component
13
- *
14
- * @remarks
15
- * The component handles three states:
16
- * 1. Loading state during token exchange
17
- * 2. Success state after successful authentication
18
- * 3. Provider selection UI when not authenticated
19
- *
20
- * @example
21
- * ```tsx
22
- * const config = { projectId: 'your-project-id', dataset: 'production' }
23
- * return <LoginLinks sanityInstance={config} />
24
- * ```
25
- */
26
- export const LoginLinks = (): ReactElement => {
27
- const loginUrls = useLoginUrls()
28
- const authState = useAuthState()
29
- useHandleAuthCallback()
30
-
31
- if (authState.type === 'logging-in') {
32
- return <div className="sc-login-links__logging-in">Logging in...</div>
33
- }
34
-
35
- // Show success state after authentication
36
- if (authState.type === 'logged-in') {
37
- return <div className="sc-login-links__logged-in">You are logged in</div>
38
- }
39
-
40
- /**
41
- * Render provider selection UI
42
- */
43
- return (
44
- <div className="sc-login-links">
45
- <h2 className="sc-login-links__title">Choose login provider</h2>
46
-
47
- <ul className="sc-login-links__list">
48
- {loginUrls.map((provider, index) => (
49
- <li key={`${provider.url}_${index}`} className="sc-login-links__item">
50
- <a href={provider.url} className="sc-login-links__link">
51
- {provider.title}
52
- </a>
53
- </li>
54
- ))}
55
- </ul>
56
- </div>
57
- )
58
- }
@@ -1,27 +0,0 @@
1
- import {screen} from '@testing-library/react'
2
- import {describe, expect, it, vi} from 'vitest'
3
-
4
- import {renderWithWrappers} from './authTestHelpers'
5
- import {Login} from './Login'
6
-
7
- vi.mock('../../hooks/auth/useLoginUrls', () => ({
8
- useLoginUrls: vi.fn(() => [
9
- {title: 'Provider A', url: 'https://provider-a.com/auth'},
10
- {title: 'Provider B', url: 'https://provider-b.com/auth'},
11
- ]),
12
- }))
13
-
14
- describe('Login', () => {
15
- it('renders login providers', () => {
16
- renderWithWrappers(<Login />)
17
- expect(screen.getByText('Choose login provider')).toBeInTheDocument()
18
- expect(screen.getByRole('link', {name: 'Provider A'})).toHaveAttribute(
19
- 'href',
20
- 'https://provider-a.com/auth',
21
- )
22
- expect(screen.getByRole('link', {name: 'Provider B'})).toHaveAttribute(
23
- 'href',
24
- 'https://provider-b.com/auth',
25
- )
26
- })
27
- })
@@ -1,39 +0,0 @@
1
- import {type JSX, Suspense} from 'react'
2
-
3
- import {useLoginUrls} from '../../hooks/auth/useLoginUrls'
4
- import {LoginLayout, type LoginLayoutProps} from './LoginLayout'
5
-
6
- /**
7
- * Login component that displays available authentication providers.
8
- * Renders a list of login options with a loading fallback while providers load.
9
- *
10
- * @alpha
11
- * @internal
12
- */
13
- export function Login({header, footer}: LoginLayoutProps): JSX.Element {
14
- return (
15
- <LoginLayout header={header} footer={footer}>
16
- <div className="sc-login">
17
- <h1 className="sc-login__title">Choose login provider</h1>
18
-
19
- <Suspense fallback={<div className="sc-login__loading">Loading…</div>}>
20
- <Providers />
21
- </Suspense>
22
- </div>
23
- </LoginLayout>
24
- )
25
- }
26
-
27
- function Providers() {
28
- const loginUrls = useLoginUrls()
29
-
30
- return (
31
- <div className="sc-login-providers">
32
- {loginUrls.map(({title, url}) => (
33
- <a key={url} href={url}>
34
- {title}
35
- </a>
36
- ))}
37
- </div>
38
- )
39
- }
@@ -1,19 +0,0 @@
1
- import {screen} from '@testing-library/react'
2
- import {describe, expect, it} from 'vitest'
3
-
4
- import {renderWithWrappers} from './authTestHelpers'
5
- import {LoginLayout} from './LoginLayout'
6
-
7
- describe('LoginLayout', () => {
8
- it('renders header, children, and footer', () => {
9
- renderWithWrappers(
10
- <LoginLayout header={<div>Header Content</div>} footer={<div>Footer Content</div>}>
11
- <div>Main Content</div>
12
- </LoginLayout>,
13
- )
14
-
15
- expect(screen.getByText('Header Content')).toBeInTheDocument()
16
- expect(screen.getByText('Main Content')).toBeInTheDocument()
17
- expect(screen.getByText('Footer Content')).toBeInTheDocument()
18
- })
19
- })
@@ -1,69 +0,0 @@
1
- import {LoginFooter} from './LoginFooter'
2
-
3
- /**
4
- * @alpha
5
- */
6
- export interface LoginLayoutProps {
7
- /** Optional header content rendered at top of card */
8
- header?: React.ReactNode
9
-
10
- /** Optional footer content rendered below card. Defaults to an internal login footer */
11
- footer?: React.ReactNode
12
-
13
- /** Main content rendered in card body */
14
- children?: React.ReactNode
15
- }
16
-
17
- /**
18
- * Layout component for login-related screens providing consistent styling and structure.
19
- * Renders content in a centered card with optional header and footer sections.
20
- *
21
- * Can be used to build custom login screens for the AuthBoundary component, including:
22
- * - Login provider selection (LoginComponent)
23
- * - OAuth callback handling (CallbackComponent)
24
- * - Error states (LoginErrorComponent)
25
- *
26
- * @example
27
- * ```tsx
28
- * // Custom login screen using the layout
29
- * function CustomLogin({header, footer}: LoginLayoutProps) {
30
- * return (
31
- * <LoginLayout
32
- * header={header}
33
- * footer={footer}
34
- * >
35
- * <CustomLoginContent />
36
- * </LoginLayout>
37
- * )
38
- * }
39
- *
40
- * // Use with AuthBoundary
41
- * <AuthBoundary
42
- * LoginComponent={CustomLogin}
43
- * header={<Logo />}
44
- * >
45
- * <ProtectedContent />
46
- * </AuthBoundary>
47
- * ```
48
- *
49
- * @alpha
50
- */
51
- export function LoginLayout({
52
- children,
53
- footer = <LoginFooter />,
54
- header,
55
- }: LoginLayoutProps): React.ReactNode {
56
- return (
57
- <div className="sc-login-layout">
58
- <div className="sc-login-layout__container">
59
- <div className="sc-login-layout__card">
60
- {header && <div className="sc-login-layout__card-header">{header}</div>}
61
-
62
- {children && <div className="sc-login-layout__card-body">{children}</div>}
63
- </div>
64
-
65
- {footer}
66
- </div>
67
- </div>
68
- )
69
- }
@@ -1,11 +0,0 @@
1
- import {createSanityInstance} from '@sanity/sdk'
2
- import {render, type RenderResult} from '@testing-library/react'
3
- import React from 'react'
4
-
5
- import {SanityProvider} from '../../context/SanityProvider'
6
-
7
- const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
8
-
9
- export const renderWithWrappers = (ui: React.ReactElement): RenderResult => {
10
- return render(<SanityProvider sanityInstances={[sanityInstance]}>{ui}</SanityProvider>)
11
- }
@@ -1,25 +0,0 @@
1
- import {createSanityInstance} from '@sanity/sdk'
2
- import {render} from '@testing-library/react'
3
- import {describe, expect, it} from 'vitest'
4
-
5
- import {useSanityInstance} from '../hooks/context/useSanityInstance'
6
- import {SanityProvider} from './SanityProvider'
7
-
8
- describe('SanityProvider', () => {
9
- const sanityInstance = createSanityInstance({projectId: 'test-project', dataset: 'production'})
10
-
11
- it('provides instance to nested components', () => {
12
- const TestComponent = () => {
13
- const instance = useSanityInstance()
14
- return <div data-testid="test">{instance.identity.projectId}</div>
15
- }
16
-
17
- const {getByTestId} = render(
18
- <SanityProvider sanityInstances={[sanityInstance]}>
19
- <TestComponent />
20
- </SanityProvider>,
21
- )
22
-
23
- expect(getByTestId('test')).toHaveTextContent('test-project')
24
- })
25
- })
@@ -1,50 +0,0 @@
1
- import {type SanityInstance} from '@sanity/sdk'
2
- import {type ReactElement} from 'react'
3
-
4
- import {SanityInstanceContext} from './SanityInstanceContext'
5
-
6
- /**
7
- * @internal
8
- */
9
- export interface SanityProviderProps {
10
- children: React.ReactNode
11
- sanityInstances: SanityInstance[]
12
- }
13
-
14
- /**
15
- * @internal
16
- *
17
- * Top-level context provider that provides access to the Sanity configuration instance.
18
- * This must wrap any components making use of the Sanity SDK React hooks.
19
- *
20
- * @remarks In most cases, SanityApp should be used rather than SanityProvider directly; SanityApp bundles both SanityProvider and an authentication layer.
21
- * @param props - Sanity project and dataset configuration
22
- * @returns Rendered component
23
- * @example
24
- * ```tsx
25
- * import {createSanityInstance} from '@sanity/sdk'
26
- * import {SanityProvider} from '@sanity/sdk-react'
27
- *
28
- * import MyAppRoot from './Root'
29
- *
30
- * const sanityInstance = createSanityInstance({
31
- * projectId: 'your-project-id',
32
- * dataset: 'production',
33
- * })
34
- *
35
- * export default function MyApp() {
36
- * return (
37
- * <SanityProvider sanityInstance={sanityInstance}>
38
- * <MyAppRoot />
39
- * </SanityProvider>
40
- * )
41
- * }
42
- * ```
43
- */
44
- export const SanityProvider = ({children, sanityInstances}: SanityProviderProps): ReactElement => {
45
- return (
46
- <SanityInstanceContext.Provider value={sanityInstances}>
47
- {children}
48
- </SanityInstanceContext.Provider>
49
- )
50
- }
@@ -1,68 +0,0 @@
1
- import {createSanityInstance, fetchLoginUrls, getLoginUrlsState} from '@sanity/sdk'
2
- import {renderHook} from '@testing-library/react'
3
- import {act, Suspense} from 'react'
4
- import {throwError} from 'rxjs'
5
- import {describe, expect, it, vi} from 'vitest'
6
-
7
- import {useLoginUrls} from './useLoginUrls'
8
-
9
- vi.mock('../context/useSanityInstance', () => ({
10
- useSanityInstance: vi.fn().mockReturnValue(createSanityInstance({projectId: 'p', dataset: 'd'})),
11
- }))
12
-
13
- vi.mock(import('@sanity/sdk'), async (importOriginal) => {
14
- const actual = await importOriginal()
15
- return {...actual, getLoginUrlsState: vi.fn(), fetchLoginUrls: vi.fn()}
16
- })
17
-
18
- describe('useLoginUrls', () => {
19
- it('should suspend by throwing `fetchLoginUrls` if `getLoginUrlsState().getCurrent()` is falsy', async () => {
20
- const subscribe = vi.fn()
21
- const getCurrent = vi.fn().mockReturnValue(undefined)
22
-
23
- let resolve: () => void
24
- const promise = new Promise<void>((thisResolve) => {
25
- resolve = thisResolve
26
- })
27
- vi.mocked(fetchLoginUrls).mockReturnValue(
28
- promise as unknown as ReturnType<typeof fetchLoginUrls>,
29
- )
30
-
31
- vi.mocked(getLoginUrlsState).mockReturnValue({
32
- getCurrent,
33
- subscribe,
34
- observable: throwError(() => new Error('Unexpected usage of observable')),
35
- })
36
-
37
- const wrapper = ({children}: {children: React.ReactNode}) => (
38
- <Suspense fallback={<>Loading…</>}>{children}</Suspense>
39
- )
40
-
41
- const {result, rerender} = renderHook(() => useLoginUrls(), {wrapper})
42
- expect(result.current).toEqual(null)
43
-
44
- const mockProviders = [{name: 'google', title: 'Google', url: 'http://test.com/auth/google'}]
45
-
46
- await act(async () => {
47
- getCurrent.mockReturnValue(mockProviders)
48
- resolve()
49
- rerender()
50
- await promise
51
- })
52
-
53
- expect(result.current).toEqual(mockProviders)
54
- })
55
-
56
- it('should render according to `getLoginUrlsState`', () => {
57
- const subscribe = vi.fn()
58
- const mockProviders = [{name: 'google', title: 'Google', url: 'http://test.com/auth/google'}]
59
- vi.mocked(getLoginUrlsState).mockReturnValue({
60
- getCurrent: () => mockProviders,
61
- subscribe,
62
- observable: throwError(() => new Error('Unexpected usage of observable')),
63
- })
64
-
65
- const {result} = renderHook(() => useLoginUrls())
66
- expect(result.current).toEqual(mockProviders)
67
- })
68
- })
@@ -1,52 +0,0 @@
1
- import {type AuthProvider, fetchLoginUrls, getLoginUrlsState} from '@sanity/sdk'
2
- import {useMemo, useSyncExternalStore} from 'react'
3
-
4
- import {useSanityInstance} from '../context/useSanityInstance'
5
-
6
- /**
7
- * @internal
8
- * A React hook that retrieves the available authentication provider URLs for login.
9
- *
10
- * @remarks
11
- * This hook fetches the login URLs from the Sanity auth store when the component mounts.
12
- * Each provider object contains information about an authentication method, including its URL.
13
- * The hook will suspend if the login URLs have not yet loaded.
14
- *
15
- * @example
16
- * ```tsx
17
- * // LoginProviders component that uses the hook
18
- * function LoginProviders() {
19
- * const providers = useLoginUrls()
20
- *
21
- * return (
22
- * <div>
23
- * {providers.map((provider) => (
24
- * <a key={provider.name} href={provider.url}>
25
- * Login with {provider.title}
26
- * </a>
27
- * ))}
28
- * </div>
29
- * )
30
- * }
31
- *
32
- * // Parent component with Suspense boundary
33
- * function LoginPage() {
34
- * return (
35
- * <Suspense fallback={<div>Loading authentication providers...</div>}>
36
- * <LoginProviders />
37
- * </Suspense>
38
- * )
39
- * }
40
- * ```
41
- *
42
- * @returns An array of {@link AuthProvider} objects containing login URLs and provider information
43
- * @public
44
- */
45
- export function useLoginUrls(): AuthProvider[] {
46
- const instance = useSanityInstance()
47
- const {subscribe, getCurrent} = useMemo(() => getLoginUrlsState(instance), [instance])
48
-
49
- if (!getCurrent()) throw fetchLoginUrls(instance)
50
-
51
- return useSyncExternalStore(subscribe, getCurrent as () => AuthProvider[])
52
- }