@sanity/sdk-react 0.0.0-alpha.3 → 0.0.0-alpha.30

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 (131) hide show
  1. package/README.md +6 -100
  2. package/dist/index.d.ts +2390 -2
  3. package/dist/index.js +1119 -2
  4. package/dist/index.js.map +1 -1
  5. package/package.json +35 -49
  6. package/src/_exports/index.ts +2 -10
  7. package/src/_exports/sdk-react.ts +73 -0
  8. package/src/components/SDKProvider.test.tsx +103 -0
  9. package/src/components/SDKProvider.tsx +52 -0
  10. package/src/components/SanityApp.test.tsx +244 -0
  11. package/src/components/SanityApp.tsx +106 -0
  12. package/src/components/auth/AuthBoundary.test.tsx +204 -29
  13. package/src/components/auth/AuthBoundary.tsx +96 -19
  14. package/src/components/auth/ConfigurationError.ts +22 -0
  15. package/src/components/auth/LoginCallback.test.tsx +22 -24
  16. package/src/components/auth/LoginCallback.tsx +6 -16
  17. package/src/components/auth/LoginError.test.tsx +11 -18
  18. package/src/components/auth/LoginError.tsx +43 -25
  19. package/src/components/utils.ts +22 -0
  20. package/src/context/ResourceProvider.test.tsx +157 -0
  21. package/src/context/ResourceProvider.tsx +111 -0
  22. package/src/context/SanityInstanceContext.ts +4 -0
  23. package/src/hooks/_synchronous-groq-js.mjs +4 -0
  24. package/src/hooks/auth/useAuthState.tsx +4 -5
  25. package/src/hooks/auth/useAuthToken.tsx +1 -1
  26. package/src/hooks/auth/useCurrentUser.tsx +28 -4
  27. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
  28. package/src/hooks/auth/useDashboardOrganizationId.tsx +30 -0
  29. package/src/hooks/auth/useHandleAuthCallback.test.tsx +16 -0
  30. package/src/hooks/auth/{useHandleCallback.tsx → useHandleAuthCallback.tsx} +7 -6
  31. package/src/hooks/auth/useLogOut.test.tsx +2 -2
  32. package/src/hooks/auth/useLogOut.tsx +1 -1
  33. package/src/hooks/auth/useLoginUrl.tsx +14 -0
  34. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +136 -0
  35. package/src/hooks/auth/useVerifyOrgProjects.tsx +48 -0
  36. package/src/hooks/client/useClient.ts +13 -33
  37. package/src/hooks/comlink/useFrameConnection.test.tsx +167 -0
  38. package/src/hooks/comlink/useFrameConnection.ts +107 -0
  39. package/src/hooks/comlink/useManageFavorite.test.ts +368 -0
  40. package/src/hooks/comlink/useManageFavorite.ts +210 -0
  41. package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +85 -0
  42. package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +115 -0
  43. package/src/hooks/comlink/useWindowConnection.test.ts +135 -0
  44. package/src/hooks/comlink/useWindowConnection.ts +123 -0
  45. package/src/hooks/context/useSanityInstance.test.tsx +157 -15
  46. package/src/hooks/context/useSanityInstance.ts +68 -11
  47. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +276 -0
  48. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +139 -0
  49. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +291 -0
  50. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +101 -0
  51. package/src/hooks/datasets/useDatasets.test.ts +80 -0
  52. package/src/hooks/datasets/useDatasets.ts +52 -0
  53. package/src/hooks/document/useApplyDocumentActions.test.ts +20 -0
  54. package/src/hooks/document/useApplyDocumentActions.ts +124 -0
  55. package/src/hooks/document/useDocument.test.ts +118 -0
  56. package/src/hooks/document/useDocument.ts +212 -0
  57. package/src/hooks/document/useDocumentEvent.test.ts +62 -0
  58. package/src/hooks/document/useDocumentEvent.ts +94 -0
  59. package/src/hooks/document/useDocumentPermissions.test.ts +204 -0
  60. package/src/hooks/document/useDocumentPermissions.ts +131 -0
  61. package/src/hooks/document/useDocumentSyncStatus.test.ts +23 -0
  62. package/src/hooks/document/useDocumentSyncStatus.ts +61 -0
  63. package/src/hooks/document/useEditDocument.test.ts +196 -0
  64. package/src/hooks/document/useEditDocument.ts +314 -0
  65. package/src/hooks/documents/useDocuments.test.tsx +179 -0
  66. package/src/hooks/documents/useDocuments.ts +300 -0
  67. package/src/hooks/helpers/createCallbackHook.test.tsx +2 -2
  68. package/src/hooks/helpers/createCallbackHook.tsx +1 -1
  69. package/src/hooks/helpers/createStateSourceHook.test.tsx +67 -1
  70. package/src/hooks/helpers/createStateSourceHook.tsx +27 -11
  71. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +284 -0
  72. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +353 -0
  73. package/src/hooks/preview/usePreview.test.tsx +85 -17
  74. package/src/hooks/preview/usePreview.tsx +81 -22
  75. package/src/hooks/projection/useProjection.test.tsx +283 -0
  76. package/src/hooks/projection/useProjection.ts +232 -0
  77. package/src/hooks/projects/useProject.test.ts +80 -0
  78. package/src/hooks/projects/useProject.ts +51 -0
  79. package/src/hooks/projects/useProjects.test.ts +77 -0
  80. package/src/hooks/projects/useProjects.ts +45 -0
  81. package/src/hooks/query/useQuery.test.tsx +188 -0
  82. package/src/hooks/query/useQuery.ts +193 -0
  83. package/src/hooks/releases/useActiveReleases.test.tsx +84 -0
  84. package/src/hooks/releases/useActiveReleases.ts +39 -0
  85. package/src/hooks/releases/usePerspective.test.tsx +120 -0
  86. package/src/hooks/releases/usePerspective.ts +49 -0
  87. package/src/hooks/users/useUsers.test.tsx +330 -0
  88. package/src/hooks/users/useUsers.ts +120 -0
  89. package/src/utils/getEnv.ts +21 -0
  90. package/src/version.ts +8 -0
  91. package/src/vite-env.d.ts +10 -0
  92. package/dist/_chunks-es/useLogOut.js +0 -44
  93. package/dist/_chunks-es/useLogOut.js.map +0 -1
  94. package/dist/assets/bundle-CcAyERuZ.css +0 -11
  95. package/dist/components.d.ts +0 -259
  96. package/dist/components.js +0 -301
  97. package/dist/components.js.map +0 -1
  98. package/dist/hooks.d.ts +0 -186
  99. package/dist/hooks.js +0 -81
  100. package/dist/hooks.js.map +0 -1
  101. package/src/_exports/components.ts +0 -13
  102. package/src/_exports/hooks.ts +0 -9
  103. package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +0 -113
  104. package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +0 -42
  105. package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +0 -21
  106. package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +0 -105
  107. package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +0 -42
  108. package/src/components/DocumentListLayout/DocumentListLayout.tsx +0 -12
  109. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +0 -49
  110. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +0 -39
  111. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +0 -30
  112. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +0 -171
  113. package/src/components/Login/LoginLinks.test.tsx +0 -100
  114. package/src/components/Login/LoginLinks.tsx +0 -73
  115. package/src/components/auth/Login.test.tsx +0 -41
  116. package/src/components/auth/Login.tsx +0 -45
  117. package/src/components/auth/LoginFooter.test.tsx +0 -29
  118. package/src/components/auth/LoginFooter.tsx +0 -65
  119. package/src/components/auth/LoginLayout.test.tsx +0 -33
  120. package/src/components/auth/LoginLayout.tsx +0 -81
  121. package/src/components/context/SanityProvider.test.tsx +0 -25
  122. package/src/components/context/SanityProvider.tsx +0 -42
  123. package/src/css/css.config.js +0 -220
  124. package/src/css/paramour.css +0 -2347
  125. package/src/css/styles.css +0 -11
  126. package/src/hooks/auth/useHandleCallback.test.tsx +0 -16
  127. package/src/hooks/auth/useLoginUrls.test.tsx +0 -68
  128. package/src/hooks/auth/useLoginUrls.tsx +0 -51
  129. package/src/hooks/client/useClient.test.tsx +0 -130
  130. package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
  131. package/src/hooks/documentCollection/useDocuments.ts +0 -87
@@ -1,10 +1,34 @@
1
- import {getCurrentUserState} from '@sanity/sdk'
1
+ import {type CurrentUser, getCurrentUserState} from '@sanity/sdk'
2
2
 
3
3
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
4
4
 
5
+ type UseCurrentUser = {
6
+ /**
7
+ * @public
8
+ *
9
+ * Provides the currently authenticated user’s profile information.
10
+ *
11
+ * @category Users
12
+ * @returns The current user data
13
+ *
14
+ * @example Rendering a basic user profile
15
+ * ```
16
+ * const user = useCurrentUser()
17
+ *
18
+ * return (
19
+ * <figure>
20
+ * <img src={user?.profileImage} alt=`Profile image for ${user?.name}` />
21
+ * <h2>{user?.name}</h2>
22
+ * </figure>
23
+ * )
24
+ * ```
25
+ */
26
+ (): CurrentUser | null
27
+ }
28
+
5
29
  /**
6
- * Hook to get the currently logged in user
7
30
  * @public
8
- * @returns The current user or null if not authenticated
31
+ * @function
32
+ * @TODO This should not return null — users of a custom app will always be authenticated via Core
9
33
  */
10
- export const useCurrentUser = createStateSourceHook(getCurrentUserState)
34
+ export const useCurrentUser: UseCurrentUser = createStateSourceHook(getCurrentUserState)
@@ -0,0 +1,42 @@
1
+ import {createSanityInstance, getDashboardOrganizationId} from '@sanity/sdk'
2
+ import {renderHook} from '@testing-library/react'
3
+ import {throwError} from 'rxjs'
4
+ import {describe, expect, it, vi} from 'vitest'
5
+
6
+ import {useDashboardOrganizationId} from './useDashboardOrganizationId'
7
+
8
+ vi.mock('../context/useSanityInstance', () => ({
9
+ useSanityInstance: vi.fn().mockReturnValue(createSanityInstance({projectId: 'p', dataset: 'd'})),
10
+ }))
11
+
12
+ vi.mock('@sanity/sdk', async (importOriginal) => {
13
+ const actual = await importOriginal()
14
+ return {...(actual || {}), getDashboardOrganizationId: vi.fn()}
15
+ })
16
+
17
+ describe('useDashboardOrganizationId', () => {
18
+ it('should return undefined when no organization ID is set', () => {
19
+ const subscribe = vi.fn()
20
+ vi.mocked(getDashboardOrganizationId).mockReturnValue({
21
+ getCurrent: () => undefined,
22
+ subscribe,
23
+ observable: throwError(() => new Error('Unexpected usage of observable')),
24
+ })
25
+
26
+ const {result} = renderHook(() => useDashboardOrganizationId())
27
+ expect(result.current).toBeUndefined()
28
+ })
29
+
30
+ it('should return organization ID when one is set', () => {
31
+ const subscribe = vi.fn()
32
+ const mockOrgId = 'team_123'
33
+ vi.mocked(getDashboardOrganizationId).mockReturnValue({
34
+ getCurrent: () => mockOrgId,
35
+ subscribe,
36
+ observable: throwError(() => new Error('Unexpected usage of observable')),
37
+ })
38
+
39
+ const {result} = renderHook(() => useDashboardOrganizationId())
40
+ expect(result.current).toBe(mockOrgId)
41
+ })
42
+ })
@@ -0,0 +1,30 @@
1
+ import {getDashboardOrganizationId} from '@sanity/sdk'
2
+ import {useMemo, useSyncExternalStore} from 'react'
3
+
4
+ import {useSanityInstance} from '../context/useSanityInstance'
5
+
6
+ /**
7
+ * @public
8
+ *
9
+ * A React hook that retrieves the dashboard organization ID that is currently selected in the Sanity Dashboard.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * function DashboardComponent() {
14
+ * const orgId = useDashboardOrganizationId()
15
+ *
16
+ * if (!orgId) return null
17
+ *
18
+ * return <div>Organization ID: {String(orgId)}</div>
19
+ * }
20
+ * ```
21
+ *
22
+ * @category Dashboard
23
+ * @returns The dashboard organization ID (string | undefined)
24
+ */
25
+ export function useDashboardOrganizationId(): string | undefined {
26
+ const instance = useSanityInstance()
27
+ const {subscribe, getCurrent} = useMemo(() => getDashboardOrganizationId(instance), [instance])
28
+
29
+ return useSyncExternalStore(subscribe, getCurrent)
30
+ }
@@ -0,0 +1,16 @@
1
+ import {handleAuthCallback} from '@sanity/sdk'
2
+ import {identity} from 'rxjs'
3
+ import {describe, it} from 'vitest'
4
+
5
+ import {createCallbackHook} from '../helpers/createCallbackHook'
6
+
7
+ vi.mock('../helpers/createCallbackHook', () => ({createCallbackHook: vi.fn(identity)}))
8
+ vi.mock('@sanity/sdk', () => ({handleAuthCallback: vi.fn()}))
9
+
10
+ describe('useHandleAuthCallback', () => {
11
+ it('calls `createCallbackHook` with `handleAuthCallback`', async () => {
12
+ const {useHandleAuthCallback} = await import('./useHandleAuthCallback')
13
+ expect(createCallbackHook).toHaveBeenCalledWith(handleAuthCallback)
14
+ expect(useHandleAuthCallback).toBe(handleAuthCallback)
15
+ })
16
+ })
@@ -1,15 +1,16 @@
1
- import {handleCallback} from '@sanity/sdk'
1
+ import {handleAuthCallback} from '@sanity/sdk'
2
2
 
3
3
  import {createCallbackHook} from '../helpers/createCallbackHook'
4
4
 
5
5
  /**
6
+ * @internal
6
7
  * A React hook that returns a function for handling authentication callbacks.
7
8
  *
8
9
  * @remarks
9
10
  * This hook provides access to the authentication store's callback handler,
10
11
  * which processes auth redirects by extracting the session ID and fetching the
11
12
  * authentication token. If fetching the long-lived token is successful,
12
- * `handleCallback` will return a Promise that resolves a new location that
13
+ * `handleAuthCallback` will return a Promise that resolves a new location that
13
14
  * removes the short-lived token from the URL. Use this in combination with
14
15
  * `history.replaceState` or your own router's `replace` function to update the
15
16
  * current location without triggering a reload.
@@ -17,13 +18,13 @@ import {createCallbackHook} from '../helpers/createCallbackHook'
17
18
  * @example
18
19
  * ```tsx
19
20
  * function AuthCallback() {
20
- * const handleCallback = useHandleCallback()
21
+ * const handleAuthCallback = useHandleAuthCallback()
21
22
  * const router = useRouter() // Example router
22
23
  *
23
24
  * useEffect(() => {
24
25
  * async function processCallback() {
25
26
  * // Handle the callback and get the cleaned URL
26
- * const newUrl = await handleCallback(window.location.href)
27
+ * const newUrl = await handleAuthCallback(window.location.href)
27
28
  *
28
29
  * if (newUrl) {
29
30
  * // Replace URL without triggering navigation
@@ -32,7 +33,7 @@ import {createCallbackHook} from '../helpers/createCallbackHook'
32
33
  * }
33
34
  *
34
35
  * processCallback().catch(console.error)
35
- * }, [handleCallback, router])
36
+ * }, [handleAuthCallback, router])
36
37
  *
37
38
  * return <div>Completing login...</div>
38
39
  * }
@@ -41,4 +42,4 @@ import {createCallbackHook} from '../helpers/createCallbackHook'
41
42
  * @returns A callback handler function that processes OAuth redirects
42
43
  * @public
43
44
  */
44
- export const useHandleCallback = createCallbackHook(handleCallback)
45
+ export const useHandleAuthCallback = createCallbackHook(handleAuthCallback)
@@ -7,8 +7,8 @@ import {createCallbackHook} from '../helpers/createCallbackHook'
7
7
  vi.mock('../helpers/createCallbackHook', () => ({createCallbackHook: vi.fn(identity)}))
8
8
  vi.mock('@sanity/sdk', () => ({logout: vi.fn()}))
9
9
 
10
- describe('useHandleCallback', () => {
11
- it('calls `createCallbackHook` with `handleCallback`', async () => {
10
+ describe('useLogOut', () => {
11
+ it('calls `createCallbackHook` with `logout`', async () => {
12
12
  const {useLogOut} = await import('./useLogOut')
13
13
  expect(createCallbackHook).toHaveBeenCalledWith(logout)
14
14
  expect(useLogOut).toBe(logout)
@@ -4,7 +4,7 @@ import {createCallbackHook} from '../helpers/createCallbackHook'
4
4
 
5
5
  /**
6
6
  * Hook to log out of the current session
7
- * @public
7
+ * @internal
8
8
  * @returns A function to log out of the current session
9
9
  */
10
10
  export const useLogOut = createCallbackHook(logout)
@@ -0,0 +1,14 @@
1
+ import {getLoginUrlState} from '@sanity/sdk'
2
+ import {useMemo, useSyncExternalStore} from 'react'
3
+
4
+ import {useSanityInstance} from '../context/useSanityInstance'
5
+
6
+ /**
7
+ * @internal
8
+ */
9
+ export function useLoginUrl(): string {
10
+ const instance = useSanityInstance()
11
+ const {subscribe, getCurrent} = useMemo(() => getLoginUrlState(instance), [instance])
12
+
13
+ return useSyncExternalStore(subscribe, getCurrent as () => string)
14
+ }
@@ -0,0 +1,136 @@
1
+ import {observeOrganizationVerificationState, type OrgVerificationResult} from '@sanity/sdk'
2
+ import {act, renderHook, waitFor} from '@testing-library/react'
3
+ import {Subject} from 'rxjs'
4
+ import {describe, expect, it, vi} from 'vitest'
5
+
6
+ import {useSanityInstance} from '../context/useSanityInstance'
7
+ import {useVerifyOrgProjects} from './useVerifyOrgProjects'
8
+
9
+ // Mock dependencies
10
+ vi.mock('@sanity/sdk', async (importOriginal) => {
11
+ const original = await importOriginal<typeof import('@sanity/sdk')>()
12
+ return {
13
+ ...original,
14
+ observeOrganizationVerificationState: vi.fn(),
15
+ }
16
+ })
17
+ vi.mock('../context/useSanityInstance')
18
+
19
+ describe('useVerifyOrgProjects', () => {
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ const mockInstance = {config: {}} as any // Dummy instance
22
+ const mockObserve = vi.mocked(observeOrganizationVerificationState)
23
+ const mockUseInstance = vi.mocked(useSanityInstance)
24
+ const testProjectIds = ['proj-1']
25
+
26
+ beforeEach(() => {
27
+ vi.clearAllMocks()
28
+ mockUseInstance.mockReturnValue(mockInstance)
29
+ })
30
+
31
+ it('should return null and not observe state if disabled', () => {
32
+ const {result} = renderHook(() => useVerifyOrgProjects(true, testProjectIds))
33
+
34
+ expect(result.current).toBeNull()
35
+ expect(mockObserve).not.toHaveBeenCalled()
36
+ })
37
+
38
+ it('should return null and not observe state if projectIds is missing or empty', () => {
39
+ const {result: resultUndefined} = renderHook(() => useVerifyOrgProjects(false, undefined))
40
+ expect(resultUndefined.current).toBeNull()
41
+ expect(mockObserve).not.toHaveBeenCalled()
42
+
43
+ const {result: resultEmpty} = renderHook(() => useVerifyOrgProjects(false, []))
44
+ expect(resultEmpty.current).toBeNull()
45
+ expect(mockObserve).not.toHaveBeenCalled()
46
+ })
47
+
48
+ it('should return null initially when not disabled and projectIds provided', () => {
49
+ const subject = new Subject<OrgVerificationResult>()
50
+ mockObserve.mockReturnValue(subject.asObservable())
51
+
52
+ const {result} = renderHook(() => useVerifyOrgProjects(false, testProjectIds))
53
+
54
+ expect(result.current).toBeNull()
55
+ expect(mockObserve).toHaveBeenCalledWith(mockInstance, testProjectIds)
56
+ })
57
+
58
+ it('should return null if observable emits { error: null }', async () => {
59
+ const subject = new Subject<OrgVerificationResult>()
60
+ mockObserve.mockReturnValue(subject.asObservable())
61
+
62
+ const {result} = renderHook(() => useVerifyOrgProjects(false, testProjectIds))
63
+
64
+ act(() => {
65
+ subject.next({error: null})
66
+ })
67
+
68
+ await waitFor(() => {
69
+ expect(result.current).toBeNull()
70
+ })
71
+ })
72
+
73
+ it('should return error string if observable emits { error: string }', async () => {
74
+ const subject = new Subject<OrgVerificationResult>()
75
+ const errorMessage = 'Org mismatch'
76
+ mockObserve.mockReturnValue(subject.asObservable())
77
+
78
+ const {result} = renderHook(() => useVerifyOrgProjects(false, testProjectIds))
79
+
80
+ act(() => {
81
+ subject.next({error: errorMessage})
82
+ })
83
+
84
+ await waitFor(() => {
85
+ expect(result.current).toBe(errorMessage)
86
+ })
87
+ })
88
+
89
+ it('should unsubscribe on unmount', () => {
90
+ const subject = new Subject<OrgVerificationResult>()
91
+ const unsubscribeSpy = vi.spyOn(subject, 'unsubscribe')
92
+ mockObserve.mockReturnValue(subject)
93
+
94
+ const {unmount} = renderHook(() => useVerifyOrgProjects(false, testProjectIds))
95
+
96
+ expect(unsubscribeSpy).not.toHaveBeenCalled()
97
+ unmount()
98
+ // Note: RxJS handles the inner subscription cleanup when the source (Subject) completes or errors,
99
+ // but testing library unmount should trigger the useEffect cleanup which calls unsubscribe.
100
+ // However, the spy might be on the Subject itself, not the final Subscription object.
101
+ // Let's adjust to spy on the returned subscription directly if possible, or accept this limitation.
102
+ // For now, we assume the useEffect cleanup calls unsubscribe correctly.
103
+ // We can validate subscription logic more deeply if needed.
104
+ // For this test, let's check if the observable reference still has observers.
105
+ expect(subject.observed).toBe(false) // Check if observers are gone after unmount
106
+ })
107
+
108
+ it('should clear the error if disabled becomes true', async () => {
109
+ const subject = new Subject<OrgVerificationResult>()
110
+ const errorMessage = 'Org mismatch'
111
+ mockObserve.mockReturnValue(subject.asObservable())
112
+
113
+ const {result, rerender} = renderHook(
114
+ ({disabled, pIds}) => useVerifyOrgProjects(disabled, pIds),
115
+ {
116
+ initialProps: {disabled: false, pIds: testProjectIds},
117
+ },
118
+ )
119
+
120
+ // Set initial error
121
+ act(() => {
122
+ subject.next({error: errorMessage})
123
+ })
124
+ await waitFor(() => {
125
+ expect(result.current).toBe(errorMessage)
126
+ })
127
+
128
+ // Disable the hook
129
+ rerender({disabled: true, pIds: testProjectIds})
130
+
131
+ // Error should be cleared
132
+ await waitFor(() => {
133
+ expect(result.current).toBeNull()
134
+ })
135
+ })
136
+ })
@@ -0,0 +1,48 @@
1
+ import {observeOrganizationVerificationState, type OrgVerificationResult} from '@sanity/sdk'
2
+ import {useEffect, useState} from 'react'
3
+
4
+ import {useSanityInstance} from '../context/useSanityInstance'
5
+
6
+ /**
7
+ * Hook that verifies the current projects belongs to the organization ID specified in the dashboard context.
8
+ *
9
+ * @public
10
+ * @param disabled - When true, disables verification and skips project verification API calls
11
+ * @returns Error message if the project doesn't match the organization ID, or null if all match or verification isn't needed
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * function OrgVerifier() {
16
+ * const error = useVerifyOrgProjects()
17
+ *
18
+ * if (error) {
19
+ * return <div className="error">{error}</div>
20
+ * }
21
+ *
22
+ * return <div>Organization projects verified!</div>
23
+ * }
24
+ * ```
25
+ */
26
+ export function useVerifyOrgProjects(disabled = false, projectIds?: string[]): string | null {
27
+ const instance = useSanityInstance()
28
+ const [error, setError] = useState<string | null>(null)
29
+
30
+ useEffect(() => {
31
+ if (disabled || !projectIds || projectIds.length === 0) {
32
+ if (error !== null) setError(null)
33
+ return
34
+ }
35
+
36
+ const verificationObservable$ = observeOrganizationVerificationState(instance, projectIds)
37
+
38
+ const subscription = verificationObservable$.subscribe((result: OrgVerificationResult) => {
39
+ setError(result.error)
40
+ })
41
+
42
+ return () => {
43
+ subscription.unsubscribe()
44
+ }
45
+ }, [instance, disabled, error, projectIds])
46
+
47
+ return error
48
+ }
@@ -1,23 +1,23 @@
1
- import {type SanityClient} from '@sanity/client'
2
- import {type ClientOptions, getClient, getSubscribableClient} from '@sanity/sdk'
3
- import {useCallback, useSyncExternalStore} from 'react'
1
+ import {getClientState} from '@sanity/sdk'
2
+ import {identity} from 'rxjs'
4
3
 
5
- import {useSanityInstance} from '../context/useSanityInstance'
4
+ import {createStateSourceHook} from '../helpers/createStateSourceHook'
6
5
 
7
6
  /**
8
7
  * A React hook that provides a client that subscribes to changes in your application,
9
- * such as user authentication changes.
10
8
  *
11
9
  * @remarks
12
- * The hook uses `useSyncExternalStore` to safely subscribe to changes
13
- * and ensure consistency between server and client rendering.
10
+ * This hook is intended for advanced use cases and special API calls that the React SDK
11
+ * does not yet provide hooks for. We welcome you to get in touch with us to let us know
12
+ * your use cases for this!
14
13
  *
14
+ * @category Platform
15
15
  * @returns A Sanity client
16
16
  *
17
17
  * @example
18
18
  * ```tsx
19
19
  * function MyComponent() {
20
- * const client = useClient()
20
+ * const client = useClient({apiVersion: '2024-11-12'})
21
21
  * const [document, setDocument] = useState(null)
22
22
  * useEffect(async () => {
23
23
  * const doc = client.fetch('*[_id == "myDocumentId"]')
@@ -28,29 +28,9 @@ import {useSanityInstance} from '../context/useSanityInstance'
28
28
  * ```
29
29
  *
30
30
  * @public
31
+ * @function
31
32
  */
32
- export function useClient(options: ClientOptions): SanityClient {
33
- const instance = useSanityInstance()
34
-
35
- const subscribe = useCallback(
36
- (onStoreChange: () => void) => {
37
- const client$ = getSubscribableClient(instance, options)
38
- const subscription = client$.subscribe({
39
- next: onStoreChange,
40
- error: (error) => {
41
- // @TODO: We should tackle error handling / error boundaries soon
42
- // eslint-disable-next-line no-console
43
- console.error('Error in useClient subscription:', error)
44
- },
45
- })
46
- return () => subscription.unsubscribe()
47
- },
48
- [instance, options],
49
- )
50
-
51
- const getSnapshot = useCallback(() => {
52
- return getClient(instance, options)
53
- }, [instance, options])
54
-
55
- return useSyncExternalStore(subscribe, getSnapshot)
56
- }
33
+ export const useClient = createStateSourceHook({
34
+ getState: getClientState,
35
+ getConfig: identity,
36
+ })
@@ -0,0 +1,167 @@
1
+ import {type ChannelInstance, type Controller, type Status} from '@sanity/comlink'
2
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
3
+
4
+ import {act, renderHook} from '../../../test/test-utils'
5
+ import {useFrameConnection} from './useFrameConnection'
6
+
7
+ vi.mock(import('@sanity/sdk'), async (importOriginal) => {
8
+ const actual = await importOriginal()
9
+ return {
10
+ ...actual,
11
+ getOrCreateChannel: vi.fn(),
12
+ getOrCreateController: vi.fn(),
13
+ releaseChannel: vi.fn(),
14
+ }
15
+ })
16
+
17
+ const {getOrCreateChannel, getOrCreateController, releaseChannel} = await import('@sanity/sdk')
18
+
19
+ interface TestControllerMessage {
20
+ type: 'TEST_MESSAGE'
21
+ data: {
22
+ someData: string
23
+ }
24
+ }
25
+
26
+ interface TestNodeMessage {
27
+ type: 'NODE_MESSAGE'
28
+ data: {
29
+ someData: string
30
+ }
31
+ }
32
+
33
+ describe('useFrameController', () => {
34
+ let channel: ChannelInstance<TestControllerMessage, TestNodeMessage>
35
+ let controller: Controller
36
+ let removeTargetMock: ReturnType<typeof vi.fn>
37
+ let statusCallback:
38
+ | (({status, connection}: {status: Status; connection: string}) => void)
39
+ | null = null
40
+
41
+ function createMockChannel() {
42
+ return {
43
+ on: vi.fn(() => () => {}),
44
+ post: vi.fn(),
45
+ stop: vi.fn(),
46
+ onStatus: vi.fn((callback) => {
47
+ statusCallback = callback
48
+ return () => {}
49
+ }),
50
+ } as unknown as ChannelInstance<TestControllerMessage, TestNodeMessage>
51
+ }
52
+
53
+ beforeEach(() => {
54
+ channel = createMockChannel()
55
+ removeTargetMock = vi.fn()
56
+ controller = {
57
+ addTarget: vi.fn(() => removeTargetMock),
58
+ destroy: vi.fn(),
59
+ } as unknown as Controller
60
+ vi.mocked(getOrCreateChannel).mockReturnValue(channel)
61
+ vi.mocked(getOrCreateController).mockReturnValue(controller)
62
+ })
63
+
64
+ it('should call onStatus callback when status changes', () => {
65
+ const onStatusMock = vi.fn()
66
+ renderHook(() =>
67
+ useFrameConnection({
68
+ name: 'test',
69
+ connectTo: 'iframe',
70
+ targetOrigin: '*',
71
+ onStatus: onStatusMock,
72
+ }),
73
+ )
74
+
75
+ act(() => {
76
+ statusCallback?.({status: 'connected', connection: 'test'})
77
+ })
78
+ expect(onStatusMock).toHaveBeenCalledWith('connected')
79
+
80
+ act(() => {
81
+ statusCallback?.({status: 'disconnected', connection: 'test'})
82
+ })
83
+ expect(onStatusMock).toHaveBeenCalledWith('disconnected')
84
+ })
85
+
86
+ it('should not throw if onStatus is not provided', () => {
87
+ renderHook(() =>
88
+ useFrameConnection({
89
+ name: 'test',
90
+ connectTo: 'iframe',
91
+ targetOrigin: '*',
92
+ }),
93
+ )
94
+
95
+ expect(() => {
96
+ act(() => {
97
+ statusCallback?.({status: 'connected', connection: 'test'})
98
+ })
99
+ }).not.toThrow()
100
+ })
101
+
102
+ it('should register and execute message handlers', () => {
103
+ const mockHandler = vi.fn()
104
+ const mockData = {someData: 'test'}
105
+ renderHook(() =>
106
+ useFrameConnection({
107
+ name: 'test',
108
+ connectTo: 'iframe',
109
+ targetOrigin: '*',
110
+ onMessage: {
111
+ TEST_MESSAGE: mockHandler,
112
+ },
113
+ }),
114
+ )
115
+
116
+ const onCallback = vi.mocked(channel.on).mock.calls[0][1]
117
+ onCallback(mockData)
118
+ expect(mockHandler).toHaveBeenCalledWith(mockData)
119
+ })
120
+
121
+ it('should handle connecting frames and cleanup on disconnect', () => {
122
+ const {result} = renderHook(() =>
123
+ useFrameConnection({
124
+ name: 'test',
125
+ connectTo: 'iframe',
126
+ targetOrigin: '*',
127
+ }),
128
+ )
129
+
130
+ const mockWindow = {} as Window
131
+ const cleanup = result.current.connect(mockWindow)
132
+
133
+ expect(controller.addTarget).toHaveBeenCalledWith(mockWindow)
134
+
135
+ // Test cleanup
136
+ cleanup()
137
+ expect(removeTargetMock).toHaveBeenCalled()
138
+ })
139
+
140
+ it('should send messages correctly', () => {
141
+ const {result} = renderHook(() =>
142
+ useFrameConnection<TestControllerMessage, TestNodeMessage>({
143
+ name: 'test',
144
+ connectTo: 'iframe',
145
+ targetOrigin: '*',
146
+ }),
147
+ )
148
+
149
+ const mockData = {someData: 'test'}
150
+ result.current.sendMessage('TEST_MESSAGE', mockData)
151
+
152
+ expect(channel.post).toHaveBeenCalledWith('TEST_MESSAGE', mockData)
153
+ })
154
+
155
+ it('should cleanup on unmount', () => {
156
+ const {unmount} = renderHook(() =>
157
+ useFrameConnection({
158
+ name: 'test',
159
+ connectTo: 'iframe',
160
+ targetOrigin: '*',
161
+ }),
162
+ )
163
+
164
+ unmount()
165
+ expect(releaseChannel).toHaveBeenCalled()
166
+ })
167
+ })