@sanity/sdk-react 2.2.0 → 2.3.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 (33) hide show
  1. package/dist/index.d.ts +2 -3
  2. package/dist/index.js +101 -26
  3. package/dist/index.js.map +1 -1
  4. package/package.json +7 -6
  5. package/src/components/auth/AuthBoundary.test.tsx +33 -20
  6. package/src/components/auth/AuthBoundary.tsx +20 -5
  7. package/src/components/auth/LoginError.tsx +9 -12
  8. package/src/components/errors/CorsErrorComponent.test.tsx +48 -0
  9. package/src/components/errors/CorsErrorComponent.tsx +37 -0
  10. package/src/components/errors/Error.styles.ts +35 -0
  11. package/src/components/errors/Error.tsx +40 -0
  12. package/src/context/ComlinkTokenRefresh.test.tsx +87 -38
  13. package/src/context/ComlinkTokenRefresh.tsx +2 -1
  14. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +16 -7
  15. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +56 -14
  16. package/src/hooks/dashboard/{useManageFavorite.test.ts → useManageFavorite.test.tsx} +99 -44
  17. package/src/hooks/document/{useDocument.test.ts → useDocument.test.tsx} +25 -22
  18. package/src/hooks/document/{useDocumentEvent.test.ts → useDocumentEvent.test.tsx} +17 -16
  19. package/src/hooks/document/{useDocumentPermissions.test.ts → useDocumentPermissions.test.tsx} +101 -40
  20. package/src/hooks/document/{useEditDocument.test.ts → useEditDocument.test.tsx} +52 -22
  21. package/src/hooks/documents/useDocuments.test.tsx +63 -25
  22. package/src/hooks/helpers/createCallbackHook.test.tsx +41 -37
  23. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +2 -2
  24. package/src/hooks/presence/usePresence.test.tsx +9 -6
  25. package/src/hooks/preview/useDocumentPreview.test.tsx +15 -16
  26. package/src/hooks/projection/useDocumentProjection.test.tsx +23 -38
  27. package/src/hooks/projection/useDocumentProjection.ts +3 -8
  28. package/src/hooks/query/useQuery.test.tsx +18 -10
  29. package/src/hooks/releases/useActiveReleases.test.tsx +25 -21
  30. package/src/hooks/releases/usePerspective.test.tsx +16 -22
  31. package/src/hooks/users/useUser.test.tsx +32 -15
  32. package/src/hooks/users/useUsers.test.tsx +19 -11
  33. package/src/hooks/_synchronous-groq-js.mjs +0 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk-react",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
4
4
  "private": false,
5
5
  "description": "Sanity SDK React toolkit for Content OS",
6
6
  "keywords": [
@@ -42,7 +42,7 @@
42
42
  "browserslist": "extends @sanity/browserslist-config",
43
43
  "prettier": "@sanity/prettier-config",
44
44
  "dependencies": {
45
- "@sanity/client": "^7.10.0",
45
+ "@sanity/client": "^7.12.0",
46
46
  "@sanity/message-protocol": "^0.12.0",
47
47
  "@sanity/types": "^3.83.0",
48
48
  "@types/lodash-es": "^4.17.12",
@@ -51,7 +51,7 @@
51
51
  "react-compiler-runtime": "19.1.0-rc.2",
52
52
  "react-error-boundary": "^5.0.0",
53
53
  "rxjs": "^7.8.2",
54
- "@sanity/sdk": "2.2.0"
54
+ "@sanity/sdk": "2.3.1"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@sanity/browserslist-config": "^1.0.5",
@@ -66,6 +66,7 @@
66
66
  "@vitest/coverage-v8": "3.1.2",
67
67
  "babel-plugin-react-compiler": "19.1.0-rc.1",
68
68
  "eslint": "^9.22.0",
69
+ "groq-js": "^1.19.0",
69
70
  "jsdom": "^25.0.1",
70
71
  "prettier": "^3.5.3",
71
72
  "react": "^19.1.0",
@@ -75,10 +76,10 @@
75
76
  "vite": "^6.3.4",
76
77
  "vitest": "^3.1.2",
77
78
  "@repo/config-eslint": "0.0.0",
78
- "@repo/config-test": "0.0.1",
79
79
  "@repo/package.bundle": "3.82.0",
80
- "@repo/tsconfig": "0.0.1",
81
- "@repo/package.config": "0.0.1"
80
+ "@repo/config-test": "0.0.1",
81
+ "@repo/package.config": "0.0.1",
82
+ "@repo/tsconfig": "0.0.1"
82
83
  },
83
84
  "peerDependencies": {
84
85
  "react": "^18.0.0 || ^19.0.0",
@@ -8,7 +8,6 @@ import {ResourceProvider} from '../../context/ResourceProvider'
8
8
  import {useAuthState} from '../../hooks/auth/useAuthState'
9
9
  import {useLoginUrl} from '../../hooks/auth/useLoginUrl'
10
10
  import {useVerifyOrgProjects} from '../../hooks/auth/useVerifyOrgProjects'
11
- import {useSanityInstance} from '../../hooks/context/useSanityInstance'
12
11
  import {AuthBoundary} from './AuthBoundary'
13
12
 
14
13
  // Mock hooks
@@ -23,9 +22,6 @@ vi.mock('../../hooks/auth/useHandleAuthCallback', () => ({
23
22
  vi.mock('../../hooks/auth/useLogOut', () => ({
24
23
  useLogOut: vi.fn(() => async () => {}),
25
24
  }))
26
- vi.mock('../../hooks/context/useSanityInstance', () => ({
27
- useSanityInstance: vi.fn(),
28
- }))
29
25
 
30
26
  // Mock AuthError throwing scenario
31
27
  vi.mock('./AuthError', async (importOriginal) => {
@@ -109,7 +105,6 @@ describe('AuthBoundary', () => {
109
105
  const mockUseAuthState = vi.mocked(useAuthState)
110
106
  const mockUseLoginUrl = vi.mocked(useLoginUrl)
111
107
  const mockUseVerifyOrgProjects = vi.mocked(useVerifyOrgProjects)
112
- const mockUseSanityInstance = vi.mocked(useSanityInstance)
113
108
  const testProjectIds = ['proj-test'] // Example project ID for tests
114
109
 
115
110
  // Mock Sanity instance
@@ -139,8 +134,6 @@ describe('AuthBoundary', () => {
139
134
  mockUseLoginUrl.mockReturnValue('http://example.com/login')
140
135
  // Default mock for useVerifyOrgProjects - returns null (no error)
141
136
  mockUseVerifyOrgProjects.mockImplementation(() => null)
142
- // Mock useSanityInstance to return our mock instance
143
- mockUseSanityInstance.mockReturnValue(mockSanityInstance)
144
137
  })
145
138
 
146
139
  afterEach(() => {
@@ -170,7 +163,9 @@ describe('AuthBoundary', () => {
170
163
  isExchangingToken: false,
171
164
  })
172
165
  const {container} = render(
173
- <AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>,
166
+ <ResourceProvider projectId="p" dataset="d" fallback={null}>
167
+ <AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
168
+ </ResourceProvider>,
174
169
  )
175
170
 
176
171
  // The callback screen renders null check that it renders nothing
@@ -184,7 +179,11 @@ describe('AuthBoundary', () => {
184
179
  currentUser: null,
185
180
  token: 'exampleToken',
186
181
  })
187
- render(<AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>)
182
+ render(
183
+ <ResourceProvider projectId="p" dataset="d" fallback={null}>
184
+ <AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
185
+ </ResourceProvider>,
186
+ )
188
187
 
189
188
  expect(screen.getByText('Protected Content')).toBeInTheDocument()
190
189
  })
@@ -194,7 +193,11 @@ describe('AuthBoundary', () => {
194
193
  type: AuthStateType.ERROR,
195
194
  error: new Error('test error'),
196
195
  })
197
- render(<AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>)
196
+ render(
197
+ <ResourceProvider projectId="p" dataset="d" fallback={null}>
198
+ <AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
199
+ </ResourceProvider>,
200
+ )
198
201
 
199
202
  // The AuthBoundary should throw an AuthError internally
200
203
  // and then display the LoginError component as the fallback.
@@ -207,7 +210,11 @@ describe('AuthBoundary', () => {
207
210
  })
208
211
 
209
212
  it('renders children when logged in and org verification passes', () => {
210
- render(<AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>)
213
+ render(
214
+ <ResourceProvider projectId="p" dataset="d" fallback={null}>
215
+ <AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
216
+ </ResourceProvider>,
217
+ )
211
218
  expect(screen.getByText('Protected Content')).toBeInTheDocument()
212
219
  })
213
220
 
@@ -226,9 +233,11 @@ describe('AuthBoundary', () => {
226
233
 
227
234
  // Need to catch the error thrown during render. ErrorBoundary mock handles this.
228
235
  render(
229
- <AuthBoundary verifyOrganization={true} projectIds={testProjectIds}>
230
- <div>Protected Content</div>
231
- </AuthBoundary>,
236
+ <ResourceProvider projectId="p" dataset="d" fallback={null}>
237
+ <AuthBoundary verifyOrganization={true} projectIds={testProjectIds}>
238
+ <div>Protected Content</div>
239
+ </AuthBoundary>
240
+ </ResourceProvider>,
232
241
  )
233
242
 
234
243
  // The ErrorBoundary's FallbackComponent should be rendered
@@ -256,9 +265,11 @@ describe('AuthBoundary', () => {
256
265
  })
257
266
 
258
267
  render(
259
- <AuthBoundary verifyOrganization={false} projectIds={testProjectIds}>
260
- <div>Protected Content</div>
261
- </AuthBoundary>,
268
+ <ResourceProvider projectId="p" dataset="d" fallback={null}>
269
+ <AuthBoundary verifyOrganization={false} projectIds={testProjectIds}>
270
+ <div>Protected Content</div>
271
+ </AuthBoundary>
272
+ </ResourceProvider>,
262
273
  )
263
274
 
264
275
  // Should render children because verification is disabled
@@ -279,9 +290,11 @@ describe('AuthBoundary', () => {
279
290
  mockUseVerifyOrgProjects.mockImplementation(() => null)
280
291
 
281
292
  render(
282
- <AuthBoundary projectIds={testProjectIds}>
283
- <div>Protected Content</div>
284
- </AuthBoundary>,
293
+ <ResourceProvider projectId="p" dataset="d" fallback={null}>
294
+ <AuthBoundary projectIds={testProjectIds}>
295
+ <div>Protected Content</div>
296
+ </AuthBoundary>
297
+ </ResourceProvider>,
285
298
  )
286
299
 
287
300
  await waitFor(() => {
@@ -1,4 +1,5 @@
1
- import {AuthStateType} from '@sanity/sdk'
1
+ import {CorsOriginError} from '@sanity/client'
2
+ import {AuthStateType, getCorsErrorProjectId} from '@sanity/sdk'
2
3
  import {useEffect, useMemo} from 'react'
3
4
  import {ErrorBoundary, type FallbackProps} from 'react-error-boundary'
4
5
 
@@ -6,6 +7,8 @@ import {ComlinkTokenRefreshProvider} from '../../context/ComlinkTokenRefresh'
6
7
  import {useAuthState} from '../../hooks/auth/useAuthState'
7
8
  import {useLoginUrl} from '../../hooks/auth/useLoginUrl'
8
9
  import {useVerifyOrgProjects} from '../../hooks/auth/useVerifyOrgProjects'
10
+ import {useSanityInstance} from '../../hooks/context/useSanityInstance'
11
+ import {CorsErrorComponent} from '../errors/CorsErrorComponent'
9
12
  import {isInIframe} from '../utils'
10
13
  import {AuthError} from './AuthError'
11
14
  import {ConfigurationError} from './ConfigurationError'
@@ -107,6 +110,14 @@ export function AuthBoundary({
107
110
  }: AuthBoundaryProps): React.ReactNode {
108
111
  const FallbackComponent = useMemo(() => {
109
112
  return function LoginComponentWithLayoutProps(fallbackProps: FallbackProps) {
113
+ if (fallbackProps.error instanceof CorsOriginError) {
114
+ return (
115
+ <CorsErrorComponent
116
+ {...fallbackProps}
117
+ projectId={getCorsErrorProjectId(fallbackProps.error)}
118
+ />
119
+ )
120
+ }
110
121
  return <LoginErrorComponent {...fallbackProps} />
111
122
  }
112
123
  }, [LoginErrorComponent])
@@ -144,17 +155,21 @@ function AuthSwitch({
144
155
  ...props
145
156
  }: AuthSwitchProps) {
146
157
  const authState = useAuthState()
147
- const orgError = useVerifyOrgProjects(!verifyOrganization, projectIds)
158
+ const instance = useSanityInstance()
159
+ const studioModeEnabled = instance.config.studioMode?.enabled
160
+ const disableVerifyOrg =
161
+ !verifyOrganization || studioModeEnabled || authState.type !== AuthStateType.LOGGED_IN
162
+ const orgError = useVerifyOrgProjects(disableVerifyOrg, projectIds)
148
163
 
149
164
  const isLoggedOut = authState.type === AuthStateType.LOGGED_OUT && !authState.isDestroyingSession
150
165
  const loginUrl = useLoginUrl()
151
166
 
152
167
  useEffect(() => {
153
- if (isLoggedOut && !isInIframe()) {
154
- // We don't want to redirect to login if we're in the Dashboard
168
+ if (isLoggedOut && !isInIframe() && !studioModeEnabled) {
169
+ // We don't want to redirect to login if we're in the Dashboard nor in studio mode
155
170
  window.location.href = loginUrl
156
171
  }
157
- }, [isLoggedOut, loginUrl])
172
+ }, [isLoggedOut, loginUrl, studioModeEnabled])
158
173
 
159
174
  // Only check the error if verification is enabled
160
175
  if (verifyOrganization && orgError) {
@@ -5,6 +5,7 @@ import {type FallbackProps} from 'react-error-boundary'
5
5
 
6
6
  import {useAuthState} from '../../hooks/auth/useAuthState'
7
7
  import {useLogOut} from '../../hooks/auth/useLogOut'
8
+ import {Error} from '../errors/Error'
8
9
  import {AuthError} from './AuthError'
9
10
  import {ConfigurationError} from './ConfigurationError'
10
11
  /**
@@ -59,17 +60,13 @@ export function LoginError({error, resetErrorBoundary}: LoginErrorProps): React.
59
60
  }, [authState, handleRetry, error])
60
61
 
61
62
  return (
62
- <div className="sc-login-error">
63
- <div className="sc-login-error__content">
64
- <h2 className="sc-login-error__title">
65
- {error instanceof AuthError ? 'Authentication Error' : 'Configuration Error'}
66
- </h2>
67
- <p className="sc-login-error__description">{authErrorMessage}</p>
68
- </div>
69
-
70
- <button className="sc-login-error__button" onClick={handleRetry}>
71
- Retry
72
- </button>
73
- </div>
63
+ <Error
64
+ heading={error instanceof AuthError ? 'Authentication Error' : 'Configuration Error'}
65
+ description={authErrorMessage}
66
+ cta={{
67
+ text: 'Retry',
68
+ onClick: handleRetry,
69
+ }}
70
+ />
74
71
  )
75
72
  }
@@ -0,0 +1,48 @@
1
+ import {describe, expect, it} from 'vitest'
2
+
3
+ import {render, screen} from '../../../test/test-utils'
4
+ import {CorsErrorComponent} from './CorsErrorComponent'
5
+
6
+ describe('CorsErrorComponent', () => {
7
+ it('shows origin and manage link when projectId is provided', () => {
8
+ const origin = 'https://example.com'
9
+ const originalLocation = window.location
10
+ // Redefine window.location to control origin in this test
11
+ Object.defineProperty(window, 'location', {
12
+ value: {origin},
13
+ configurable: true,
14
+ })
15
+
16
+ render(
17
+ <CorsErrorComponent
18
+ projectId="proj123"
19
+ error={new Error('nope')}
20
+ resetErrorBoundary={() => {}}
21
+ />,
22
+ )
23
+
24
+ expect(screen.getByText('Before you continue…')).toBeInTheDocument()
25
+ expect(screen.getByText(origin)).toBeInTheDocument()
26
+
27
+ const link = screen.getByRole('link', {name: 'Manage CORS configuration'}) as HTMLAnchorElement
28
+ expect(link).toBeInTheDocument()
29
+ expect(link.target).toBe('_blank')
30
+ expect(link.rel).toContain('noopener')
31
+ expect(link.href).toContain('https://sanity.io/manage/project/proj123/api')
32
+ expect(link.href).toContain('cors=add')
33
+ expect(link.href).toContain(`origin=${encodeURIComponent(origin)}`)
34
+ expect(link.href).toContain('credentials=include')
35
+
36
+ // restore
37
+ Object.defineProperty(window, 'location', {value: originalLocation})
38
+ })
39
+
40
+ it('shows error message when projectId is null', () => {
41
+ const error = new Error('some error message')
42
+ render(<CorsErrorComponent projectId={null} error={error} resetErrorBoundary={() => {}} />)
43
+
44
+ expect(screen.getByText('Before you continue…')).toBeInTheDocument()
45
+ expect(screen.getByText('some error message')).toBeInTheDocument()
46
+ expect(screen.queryByRole('link', {name: 'Manage CORS configuration'})).toBeNull()
47
+ })
48
+ })
@@ -0,0 +1,37 @@
1
+ import {useMemo} from 'react'
2
+ import {type FallbackProps} from 'react-error-boundary'
3
+
4
+ import {Error} from './Error'
5
+
6
+ type CorsErrorComponentProps = FallbackProps & {
7
+ projectId: string | null
8
+ }
9
+
10
+ export function CorsErrorComponent({projectId, error}: CorsErrorComponentProps): React.ReactNode {
11
+ const origin = window.location.origin
12
+ const corsUrl = useMemo(() => {
13
+ const url = new URL(`https://sanity.io/manage/project/${projectId}/api`)
14
+ url.searchParams.set('cors', 'add')
15
+ url.searchParams.set('origin', origin)
16
+ url.searchParams.set('credentials', 'include')
17
+ return url.toString()
18
+ }, [origin, projectId])
19
+ return (
20
+ <Error
21
+ heading="Before you continue…"
22
+ {...(projectId
23
+ ? {
24
+ description:
25
+ 'To access your content, you need to <strong>add the following URL as a CORS origin</strong> to your Sanity project.',
26
+ code: origin,
27
+ cta: {
28
+ text: 'Manage CORS configuration',
29
+ href: corsUrl,
30
+ },
31
+ }
32
+ : {
33
+ description: error?.message,
34
+ })}
35
+ />
36
+ )
37
+ }
@@ -0,0 +1,35 @@
1
+ const FONT_SANS_SERIF = `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, system-ui, sans-serif`
2
+ const FONT_MONOSPACE = `-apple-system-ui-monospace, 'SF Mono', Menlo, Monaco, Consolas, monospace`
3
+
4
+ const styles: Record<string, React.CSSProperties> = {
5
+ container: {
6
+ padding: '28px',
7
+ fontFamily: FONT_SANS_SERIF,
8
+ display: 'flex',
9
+ flexDirection: 'column',
10
+ gap: '21px',
11
+ fontSize: '14px',
12
+ },
13
+ heading: {
14
+ margin: 0,
15
+ fontSize: '28px',
16
+ fontWeight: 700,
17
+ },
18
+ paragraph: {
19
+ margin: 0,
20
+ },
21
+ link: {
22
+ appearance: 'none',
23
+ background: 'transparent',
24
+ border: 0,
25
+ padding: 0,
26
+ font: 'inherit',
27
+ textDecoration: 'underline',
28
+ cursor: 'pointer',
29
+ },
30
+ code: {
31
+ fontFamily: FONT_MONOSPACE,
32
+ },
33
+ }
34
+
35
+ export default styles
@@ -0,0 +1,40 @@
1
+ import styles from './Error.styles'
2
+
3
+ type ErrorProps = {
4
+ heading: string
5
+ description?: string
6
+ code?: string
7
+ cta?: {
8
+ text: string
9
+ href?: string
10
+ onClick?: () => void
11
+ }
12
+ }
13
+
14
+ export function Error({heading, description, code, cta}: ErrorProps): React.ReactNode {
15
+ return (
16
+ <div style={styles['container']}>
17
+ <h1 style={styles['heading']}>{heading}</h1>
18
+
19
+ {description && (
20
+ <p style={styles['paragraph']} dangerouslySetInnerHTML={{__html: description}} />
21
+ )}
22
+
23
+ {code && <code style={styles['code']}>{code}</code>}
24
+
25
+ {cta && (cta.href || cta.onClick) && (
26
+ <p style={styles['paragraph']}>
27
+ {cta.href ? (
28
+ <a style={styles['link']} href={cta.href} target="_blank" rel="noopener noreferrer">
29
+ {cta.text}
30
+ </a>
31
+ ) : (
32
+ <button style={styles['link']} onClick={cta.onClick}>
33
+ {cta.text}
34
+ </button>
35
+ )}
36
+ </p>
37
+ )}
38
+ </div>
39
+ )
40
+ }
@@ -7,6 +7,7 @@ import {useAuthState} from '../hooks/auth/useAuthState'
7
7
  import {useWindowConnection} from '../hooks/comlink/useWindowConnection'
8
8
  import {useSanityInstance} from '../hooks/context/useSanityInstance'
9
9
  import {ComlinkTokenRefreshProvider} from './ComlinkTokenRefresh'
10
+ import {ResourceProvider} from './ResourceProvider'
10
11
 
11
12
  // Mocks
12
13
  vi.mock('@sanity/sdk', async () => {
@@ -35,11 +36,15 @@ const mockGetIsInDashboardState = getIsInDashboardState as Mock
35
36
  const mockSetAuthToken = setAuthToken as Mock
36
37
  const mockUseAuthState = useAuthState as Mock
37
38
  const mockUseWindowConnection = useWindowConnection as Mock
38
- const mockUseSanityInstance = useSanityInstance as Mock
39
+ const mockUseSanityInstance = useSanityInstance as unknown as Mock
39
40
 
40
41
  const mockFetch = vi.fn()
41
42
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
- const mockSanityInstance: any = {projectId: 'test', dataset: 'test'}
43
+ const mockSanityInstance: any = {
44
+ projectId: 'test',
45
+ dataset: 'test',
46
+ config: {studioMode: {enabled: false}},
47
+ }
43
48
 
44
49
  describe('ComlinkTokenRefresh', () => {
45
50
  beforeEach(() => {
@@ -64,9 +69,11 @@ describe('ComlinkTokenRefresh', () => {
64
69
  it('should not request new token on 401 if not in dashboard', async () => {
65
70
  mockUseAuthState.mockReturnValue({type: AuthStateType.LOGGED_IN})
66
71
  const {rerender} = render(
67
- <ComlinkTokenRefreshProvider>
68
- <div>Test</div>
69
- </ComlinkTokenRefreshProvider>,
72
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
73
+ <ComlinkTokenRefreshProvider>
74
+ <div>Test</div>
75
+ </ComlinkTokenRefreshProvider>
76
+ </ResourceProvider>,
70
77
  )
71
78
 
72
79
  mockUseAuthState.mockReturnValue({
@@ -75,9 +82,11 @@ describe('ComlinkTokenRefresh', () => {
75
82
  })
76
83
  act(() => {
77
84
  rerender(
78
- <ComlinkTokenRefreshProvider>
79
- <div>Test</div>
80
- </ComlinkTokenRefreshProvider>,
85
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
86
+ <ComlinkTokenRefreshProvider>
87
+ <div>Test</div>
88
+ </ComlinkTokenRefreshProvider>
89
+ </ResourceProvider>,
81
90
  )
82
91
  })
83
92
 
@@ -93,11 +102,14 @@ describe('ComlinkTokenRefresh', () => {
93
102
  mockGetIsInDashboardState.mockReturnValue({getCurrent: () => true})
94
103
  })
95
104
 
96
- it('should initialize useWindowConnection with correct parameters', () => {
105
+ it('should initialize useWindowConnection with correct parameters when not in studio mode', () => {
106
+ // Simulate studio mode disabled by default
97
107
  render(
98
- <ComlinkTokenRefreshProvider>
99
- <div>Test</div>
100
- </ComlinkTokenRefreshProvider>,
108
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
109
+ <ComlinkTokenRefreshProvider>
110
+ <div>Test</div>
111
+ </ComlinkTokenRefreshProvider>
112
+ </ResourceProvider>,
101
113
  )
102
114
 
103
115
  expect(mockUseWindowConnection).toHaveBeenCalledWith(
@@ -108,7 +120,7 @@ describe('ComlinkTokenRefresh', () => {
108
120
  )
109
121
  })
110
122
 
111
- it('should handle received token', async () => {
123
+ it('should handle received token when not in studio mode', async () => {
112
124
  mockUseAuthState.mockReturnValue({
113
125
  type: AuthStateType.ERROR,
114
126
  error: {statusCode: 401, message: 'Unauthorized'},
@@ -116,20 +128,22 @@ describe('ComlinkTokenRefresh', () => {
116
128
  mockFetch.mockResolvedValueOnce({token: 'new-token'})
117
129
 
118
130
  render(
119
- <ComlinkTokenRefreshProvider>
120
- <div>Test</div>
121
- </ComlinkTokenRefreshProvider>,
131
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
132
+ <ComlinkTokenRefreshProvider>
133
+ <div>Test</div>
134
+ </ComlinkTokenRefreshProvider>
135
+ </ResourceProvider>,
122
136
  )
123
137
 
124
138
  await act(async () => {
125
139
  await vi.advanceTimersByTimeAsync(100)
126
140
  })
127
141
 
128
- expect(mockSetAuthToken).toHaveBeenCalledWith(mockSanityInstance, 'new-token')
142
+ expect(mockSetAuthToken).toHaveBeenCalledWith(expect.any(Object), 'new-token')
129
143
  expect(mockFetch).toHaveBeenCalledTimes(1)
130
144
  })
131
145
 
132
- it('should not set auth token if received token is null', async () => {
146
+ it('should not set auth token if received token is null when not in studio mode', async () => {
133
147
  mockUseAuthState.mockReturnValue({
134
148
  type: AuthStateType.ERROR,
135
149
  error: {statusCode: 401, message: 'Unauthorized'},
@@ -137,9 +151,11 @@ describe('ComlinkTokenRefresh', () => {
137
151
  mockFetch.mockResolvedValueOnce({token: null})
138
152
 
139
153
  render(
140
- <ComlinkTokenRefreshProvider>
141
- <div>Test</div>
142
- </ComlinkTokenRefreshProvider>,
154
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
155
+ <ComlinkTokenRefreshProvider>
156
+ <div>Test</div>
157
+ </ComlinkTokenRefreshProvider>
158
+ </ResourceProvider>,
143
159
  )
144
160
 
145
161
  await act(async () => {
@@ -149,7 +165,7 @@ describe('ComlinkTokenRefresh', () => {
149
165
  expect(mockSetAuthToken).not.toHaveBeenCalled()
150
166
  })
151
167
 
152
- it('should handle fetch errors gracefully', async () => {
168
+ it('should handle fetch errors gracefully when not in studio mode', async () => {
153
169
  mockUseAuthState.mockReturnValue({
154
170
  type: AuthStateType.ERROR,
155
171
  error: {statusCode: 401, message: 'Unauthorized'},
@@ -157,9 +173,11 @@ describe('ComlinkTokenRefresh', () => {
157
173
  mockFetch.mockRejectedValueOnce(new Error('Fetch failed'))
158
174
 
159
175
  render(
160
- <ComlinkTokenRefreshProvider>
161
- <div>Test</div>
162
- </ComlinkTokenRefreshProvider>,
176
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
177
+ <ComlinkTokenRefreshProvider>
178
+ <div>Test</div>
179
+ </ComlinkTokenRefreshProvider>
180
+ </ResourceProvider>,
163
181
  )
164
182
 
165
183
  await act(async () => {
@@ -170,12 +188,15 @@ describe('ComlinkTokenRefresh', () => {
170
188
  })
171
189
 
172
190
  describe('Automatic token refresh', () => {
173
- it('should not request new token for non-401 errors', async () => {
191
+ it('should not request new token for non-401 errors when not in studio mode', async () => {
174
192
  mockUseAuthState.mockReturnValue({type: AuthStateType.LOGGED_IN})
175
193
  const {rerender} = render(
176
- <ComlinkTokenRefreshProvider>
177
- <div>Test</div>
178
- </ComlinkTokenRefreshProvider>,
194
+ <ResourceProvider fallback={null}>
195
+ <ComlinkTokenRefreshProvider>
196
+ <div>Test</div>
197
+ </ComlinkTokenRefreshProvider>
198
+ ,
199
+ </ResourceProvider>,
179
200
  )
180
201
 
181
202
  mockUseAuthState.mockReturnValue({
@@ -184,9 +205,11 @@ describe('ComlinkTokenRefresh', () => {
184
205
  })
185
206
  act(() => {
186
207
  rerender(
187
- <ComlinkTokenRefreshProvider>
188
- <div>Test</div>
189
- </ComlinkTokenRefreshProvider>,
208
+ <ResourceProvider fallback={null}>
209
+ <ComlinkTokenRefreshProvider>
210
+ <div>Test</div>
211
+ </ComlinkTokenRefreshProvider>
212
+ </ResourceProvider>,
190
213
  )
191
214
  })
192
215
 
@@ -196,24 +219,50 @@ describe('ComlinkTokenRefresh', () => {
196
219
  expect(mockFetch).not.toHaveBeenCalled()
197
220
  })
198
221
 
199
- it('should request new token on LOGGED_OUT state', async () => {
222
+ it('should request new token on LOGGED_OUT state when not in studio mode', async () => {
200
223
  mockUseAuthState.mockReturnValue({type: AuthStateType.LOGGED_IN})
201
224
  const {rerender} = render(
202
- <ComlinkTokenRefreshProvider>
203
- <div>Test</div>
204
- </ComlinkTokenRefreshProvider>,
225
+ <ResourceProvider fallback={null}>
226
+ <ComlinkTokenRefreshProvider>
227
+ <div>Test</div>
228
+ </ComlinkTokenRefreshProvider>
229
+ </ResourceProvider>,
205
230
  )
206
231
 
207
232
  mockUseAuthState.mockReturnValue({type: AuthStateType.LOGGED_OUT})
208
233
  act(() => {
209
234
  rerender(
235
+ <ResourceProvider fallback={null}>
236
+ <ComlinkTokenRefreshProvider>
237
+ <div>Test</div>
238
+ </ComlinkTokenRefreshProvider>
239
+ </ResourceProvider>,
240
+ )
241
+ })
242
+
243
+ expect(mockFetch).toHaveBeenCalledWith('dashboard/v1/auth/tokens/create')
244
+ })
245
+
246
+ describe('when in studio mode', () => {
247
+ beforeEach(() => {
248
+ // Make the instance report studio mode enabled
249
+ mockUseSanityInstance.mockReturnValue({
250
+ ...mockSanityInstance,
251
+ config: {studioMode: {enabled: true}},
252
+ })
253
+ })
254
+
255
+ it('should not render DashboardTokenRefresh when studio mode enabled', () => {
256
+ render(
210
257
  <ComlinkTokenRefreshProvider>
211
258
  <div>Test</div>
212
259
  </ComlinkTokenRefreshProvider>,
213
260
  )
214
- })
215
261
 
216
- expect(mockFetch).toHaveBeenCalledWith('dashboard/v1/auth/tokens/create')
262
+ // In studio mode, provider should return children directly
263
+ // So window connection should not be initialized
264
+ expect(mockUseWindowConnection).not.toHaveBeenCalled()
265
+ })
217
266
  })
218
267
  })
219
268
  })