@sanity/sdk-react 2.6.0 → 2.8.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk-react",
3
- "version": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "private": false,
5
5
  "description": "Sanity SDK React toolkit for Content OS",
6
6
  "keywords": [
@@ -43,7 +43,7 @@
43
43
  "browserslist": "extends @sanity/browserslist-config",
44
44
  "prettier": "@sanity/prettier-config",
45
45
  "dependencies": {
46
- "@sanity/client": "^7.12.0",
46
+ "@sanity/client": "^7.14.1",
47
47
  "@sanity/message-protocol": "^0.18.0",
48
48
  "@sanity/types": "^5.2.0",
49
49
  "@types/lodash-es": "^4.17.12",
@@ -52,7 +52,7 @@
52
52
  "react-compiler-runtime": "19.1.0-rc.2",
53
53
  "react-error-boundary": "^5.0.0",
54
54
  "rxjs": "^7.8.2",
55
- "@sanity/sdk": "2.6.0"
55
+ "@sanity/sdk": "2.8.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@sanity/browserslist-config": "^1.0.5",
@@ -77,9 +77,9 @@
77
77
  "typescript": "^5.8.3",
78
78
  "vite": "^6.3.4",
79
79
  "vitest": "^3.2.4",
80
+ "@repo/package.bundle": "3.82.0",
80
81
  "@repo/config-eslint": "0.0.0",
81
82
  "@repo/config-test": "0.0.1",
82
- "@repo/package.bundle": "3.82.0",
83
83
  "@repo/package.config": "0.0.1",
84
84
  "@repo/tsconfig": "0.0.1"
85
85
  },
@@ -7,6 +7,7 @@ export {SDKProvider, type SDKProviderProps} from '../components/SDKProvider'
7
7
  export {ComlinkTokenRefreshProvider} from '../context/ComlinkTokenRefresh'
8
8
  export {renderSanityApp} from '../context/renderSanityApp'
9
9
  export {ResourceProvider, type ResourceProviderProps} from '../context/ResourceProvider'
10
+ export {SDKStudioContext, type StudioWorkspaceHandle} from '../context/SDKStudioContext'
10
11
  export {
11
12
  useAgentGenerate,
12
13
  useAgentPatch,
@@ -207,6 +207,37 @@ describe('SanityApp', () => {
207
207
  consoleWarnSpy.mockRestore()
208
208
  })
209
209
 
210
+ it('redirects to core if config is omitted and no studio context is available', async () => {
211
+ const originalLocation = window.location
212
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
213
+
214
+ const mockLocation = {
215
+ replace: vi.fn(),
216
+ href: 'http://sanity-test.app',
217
+ }
218
+
219
+ Object.defineProperty(window, 'location', {
220
+ value: mockLocation,
221
+ writable: true,
222
+ })
223
+
224
+ render(
225
+ <SanityApp fallback={<div>Fallback</div>}>
226
+ <div>Test Child</div>
227
+ </SanityApp>,
228
+ )
229
+
230
+ await new Promise((resolve) => setTimeout(resolve, 1010))
231
+
232
+ expect(mockLocation.replace).toHaveBeenCalledWith('https://sanity.io/welcome')
233
+
234
+ Object.defineProperty(window, 'location', {
235
+ value: originalLocation,
236
+ writable: true,
237
+ })
238
+ consoleWarnSpy.mockRestore()
239
+ })
240
+
210
241
  it('does not redirect to core if not inside iframe and local url', async () => {
211
242
  const originalLocation = window.location
212
243
 
@@ -244,7 +275,7 @@ describe('SanityApp', () => {
244
275
  })
245
276
  })
246
277
 
247
- it('does not redirect to core if studioMode is enabled', async () => {
278
+ it('does not redirect to core if studio config is provided', async () => {
248
279
  const originalLocation = window.location
249
280
  const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
250
281
 
@@ -256,7 +287,7 @@ describe('SanityApp', () => {
256
287
  const mockSanityConfig: SanityConfig = {
257
288
  projectId: 'test-project',
258
289
  dataset: 'test-dataset',
259
- studioMode: {enabled: true},
290
+ studio: {},
260
291
  }
261
292
 
262
293
  Object.defineProperty(window, 'location', {
@@ -283,4 +314,43 @@ describe('SanityApp', () => {
283
314
  })
284
315
  consoleWarnSpy.mockRestore()
285
316
  })
317
+
318
+ it('does not redirect to core when studio config is provided', async () => {
319
+ const originalLocation = window.location
320
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
321
+
322
+ const mockLocation = {
323
+ replace: vi.fn(),
324
+ href: 'http://sanity-test.app',
325
+ }
326
+
327
+ const mockSanityConfig: SanityConfig = {
328
+ projectId: 'test-project',
329
+ dataset: 'test-dataset',
330
+ studio: {},
331
+ }
332
+
333
+ Object.defineProperty(window, 'location', {
334
+ value: mockLocation,
335
+ writable: true,
336
+ })
337
+
338
+ render(
339
+ <SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
340
+ <div>Test Child</div>
341
+ </SanityApp>,
342
+ )
343
+
344
+ // Wait for 1 second
345
+ await new Promise((resolve) => setTimeout(resolve, 1010))
346
+
347
+ expect(mockLocation.replace).not.toHaveBeenCalled()
348
+
349
+ // Clean up the mock
350
+ Object.defineProperty(window, 'location', {
351
+ value: originalLocation,
352
+ writable: true,
353
+ })
354
+ consoleWarnSpy.mockRestore()
355
+ })
286
356
  })
@@ -1,6 +1,7 @@
1
- import {type DocumentSource, type SanityConfig} from '@sanity/sdk'
2
- import {type ReactElement, useEffect} from 'react'
1
+ import {type DocumentSource, isStudioConfig, type SanityConfig} from '@sanity/sdk'
2
+ import {type ReactElement, useContext, useEffect, useMemo} from 'react'
3
3
 
4
+ import {SDKStudioContext, type StudioWorkspaceHandle} from '../context/SDKStudioContext'
4
5
  import {SDKProvider} from './SDKProvider'
5
6
  import {isInIframe, isLocalUrl} from './utils'
6
7
 
@@ -9,8 +10,13 @@ import {isInIframe, isLocalUrl} from './utils'
9
10
  * @category Types
10
11
  */
11
12
  export interface SanityAppProps {
12
- /* One or more SanityConfig objects providing a project ID and dataset name */
13
- config: SanityConfig | SanityConfig[]
13
+ /**
14
+ * One or more SanityConfig objects providing a project ID and dataset name.
15
+ * Optional when `SanityApp` is rendered inside an `SDKStudioContext` provider
16
+ * (e.g. inside Sanity Studio) — the config is derived from the workspace
17
+ * automatically.
18
+ */
19
+ config?: SanityConfig | SanityConfig[]
14
20
  /** @deprecated use the `config` prop instead. */
15
21
  sanityConfigs?: SanityConfig[]
16
22
  sources?: Record<string, DocumentSource>
@@ -21,6 +27,21 @@ export interface SanityAppProps {
21
27
 
22
28
  const REDIRECT_URL = 'https://sanity.io/welcome'
23
29
 
30
+ /**
31
+ * Derive a SanityConfig from a Studio workspace handle.
32
+ * Maps the workspace's projectId, dataset, and reactive auth token into
33
+ * the SDK's config shape.
34
+ */
35
+ function deriveConfigFromWorkspace(workspace: StudioWorkspaceHandle): SanityConfig {
36
+ return {
37
+ projectId: workspace.projectId,
38
+ dataset: workspace.dataset,
39
+ studio: {
40
+ auth: workspace.auth.token ? {token: workspace.auth.token} : undefined,
41
+ },
42
+ }
43
+ }
44
+
24
45
  /**
25
46
  * @public
26
47
  *
@@ -29,12 +50,18 @@ const REDIRECT_URL = 'https://sanity.io/welcome'
29
50
  * must be wrapped with the SanityApp component to function properly.
30
51
  *
31
52
  * The `config` prop on the SanityApp component accepts either a single {@link SanityConfig} object, or an array of them.
32
- * This allows your app to work with one or more of your organizations datasets.
53
+ * This allows your app to work with one or more of your organization's datasets.
54
+ *
55
+ * When rendered inside a Sanity Studio that provides `SDKStudioContext`, the `config` prop is
56
+ * optional — `SanityApp` will automatically derive `projectId`, `dataset`, and auth from the
57
+ * Studio workspace.
33
58
  *
34
59
  * @remarks
35
60
  * When passing multiple SanityConfig objects to the `config` prop, the first configuration in the array becomes the default
36
61
  * configuration used by the App SDK Hooks.
37
62
  *
63
+ * When both `config` and `SDKStudioContext` are available, the explicit `config` takes precedence.
64
+ *
38
65
  * @category Components
39
66
  * @param props - Your Sanity configuration and the React children to render
40
67
  * @returns Your Sanity application, integrated with your Sanity configuration and application context
@@ -82,14 +109,29 @@ const REDIRECT_URL = 'https://sanity.io/welcome'
82
109
  export function SanityApp({
83
110
  children,
84
111
  fallback,
85
- config = [],
112
+ config: configProp,
86
113
  ...props
87
114
  }: SanityAppProps): ReactElement {
115
+ const studioWorkspace = useContext(SDKStudioContext)
116
+
117
+ // Derive config: explicit config takes precedence, then Studio context
118
+ const resolvedConfig = useMemo(() => {
119
+ if (configProp) return configProp
120
+ if (studioWorkspace) return deriveConfigFromWorkspace(studioWorkspace)
121
+ return []
122
+ }, [configProp, studioWorkspace])
123
+
88
124
  useEffect(() => {
89
125
  let timeout: NodeJS.Timeout | undefined
90
- const primaryConfig = Array.isArray(config) ? config[0] : config
126
+ const primaryConfig = Array.isArray(resolvedConfig) ? resolvedConfig[0] : resolvedConfig
127
+ const shouldRedirectWithoutConfig =
128
+ configProp === undefined && !studioWorkspace && !primaryConfig
91
129
 
92
- if (!isInIframe() && !isLocalUrl(window) && !primaryConfig?.studioMode?.enabled) {
130
+ if (
131
+ !isInIframe() &&
132
+ !isLocalUrl(window) &&
133
+ (shouldRedirectWithoutConfig || (!!primaryConfig && !isStudioConfig(primaryConfig)))
134
+ ) {
93
135
  // If the app is not running in an iframe and is not a local url, redirect to core.
94
136
  timeout = setTimeout(() => {
95
137
  // eslint-disable-next-line no-console
@@ -98,10 +140,10 @@ export function SanityApp({
98
140
  }, 1000)
99
141
  }
100
142
  return () => clearTimeout(timeout)
101
- }, [config])
143
+ }, [configProp, resolvedConfig, studioWorkspace])
102
144
 
103
145
  return (
104
- <SDKProvider {...props} fallback={fallback} config={config}>
146
+ <SDKProvider {...props} fallback={fallback} config={resolvedConfig}>
105
147
  {children}
106
148
  </SDKProvider>
107
149
  )
@@ -22,6 +22,9 @@ vi.mock('../../hooks/auth/useHandleAuthCallback', () => ({
22
22
  vi.mock('../../hooks/auth/useLogOut', () => ({
23
23
  useLogOut: vi.fn(() => async () => {}),
24
24
  }))
25
+ vi.mock('../../hooks/comlink/useWindowConnection', () => ({
26
+ useWindowConnection: vi.fn(() => ({fetch: vi.fn()})),
27
+ }))
25
28
 
26
29
  // Mock AuthError throwing scenario
27
30
  vi.mock('./AuthError', async (importOriginal) => {
@@ -1,5 +1,5 @@
1
1
  import {CorsOriginError} from '@sanity/client'
2
- import {AuthStateType, getCorsErrorProjectId} from '@sanity/sdk'
2
+ import {AuthStateType, getCorsErrorProjectId, isStudioConfig} from '@sanity/sdk'
3
3
  import {useEffect, useMemo} from 'react'
4
4
  import {ErrorBoundary, type FallbackProps} from 'react-error-boundary'
5
5
 
@@ -156,20 +156,20 @@ function AuthSwitch({
156
156
  }: AuthSwitchProps) {
157
157
  const authState = useAuthState()
158
158
  const instance = useSanityInstance()
159
- const studioModeEnabled = instance.config.studioMode?.enabled
159
+ const isStudio = isStudioConfig(instance.config)
160
160
  const disableVerifyOrg =
161
- !verifyOrganization || studioModeEnabled || authState.type !== AuthStateType.LOGGED_IN
161
+ !verifyOrganization || isStudio || authState.type !== AuthStateType.LOGGED_IN
162
162
  const orgError = useVerifyOrgProjects(disableVerifyOrg, projectIds)
163
163
 
164
164
  const isLoggedOut = authState.type === AuthStateType.LOGGED_OUT && !authState.isDestroyingSession
165
165
  const loginUrl = useLoginUrl()
166
166
 
167
167
  useEffect(() => {
168
- if (isLoggedOut && !isInIframe() && !studioModeEnabled) {
168
+ if (isLoggedOut && !isInIframe() && !isStudio) {
169
169
  // We don't want to redirect to login if we're in the Dashboard nor in studio mode
170
170
  window.location.href = loginUrl
171
171
  }
172
- }, [isLoggedOut, loginUrl, studioModeEnabled])
172
+ }, [isLoggedOut, loginUrl, isStudio])
173
173
 
174
174
  // Only check the error if verification is enabled
175
175
  if (verifyOrganization && orgError) {
@@ -9,6 +9,10 @@ vi.mock('../../hooks/auth/useLogOut', () => ({
9
9
  useLogOut: vi.fn(() => async () => {}),
10
10
  }))
11
11
 
12
+ vi.mock('../../hooks/comlink/useWindowConnection', () => ({
13
+ useWindowConnection: vi.fn(() => ({fetch: vi.fn()})),
14
+ }))
15
+
12
16
  describe('LoginError', () => {
13
17
  it('shows authentication error and retry button', async () => {
14
18
  const mockReset = vi.fn()
@@ -21,6 +25,7 @@ describe('LoginError', () => {
21
25
  )
22
26
 
23
27
  expect(screen.getByText('Authentication Error')).toBeInTheDocument()
28
+
24
29
  const retryButton = screen.getByRole('button', {name: 'Retry'})
25
30
  fireEvent.click(retryButton)
26
31
 
@@ -1,4 +1,5 @@
1
1
  import {ClientError} from '@sanity/client'
2
+ import {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
2
3
  import {
3
4
  AuthStateType,
4
5
  getClientErrorApiBody,
@@ -10,6 +11,8 @@ import {type FallbackProps} from 'react-error-boundary'
10
11
 
11
12
  import {useAuthState} from '../../hooks/auth/useAuthState'
12
13
  import {useLogOut} from '../../hooks/auth/useLogOut'
14
+ import {useWindowConnection} from '../../hooks/comlink/useWindowConnection'
15
+ import {useSanityInstance} from '../../hooks/context/useSanityInstance'
13
16
  import {Error} from '../errors/Error'
14
17
  import {AuthError} from './AuthError'
15
18
  import {ConfigurationError} from './ConfigurationError'
@@ -36,12 +39,23 @@ export function LoginError({error, resetErrorBoundary}: LoginErrorProps): React.
36
39
 
37
40
  const logout = useLogOut()
38
41
  const authState = useAuthState()
42
+ const {
43
+ config: {projectId},
44
+ } = useSanityInstance()
39
45
 
40
46
  const [authErrorMessage, setAuthErrorMessage] = useState(
41
47
  'Please try again or contact support if the problem persists.',
42
48
  )
43
49
  const [showRetryCta, setShowRetryCta] = useState(true)
44
50
 
51
+ /**
52
+ * TODO: before merge update message-protocol package to include the new message type
53
+ */
54
+ const {fetch} = useWindowConnection({
55
+ name: SDK_NODE_NAME,
56
+ connectTo: SDK_CHANNEL_NAME,
57
+ })
58
+
45
59
  const handleRetry = useCallback(async () => {
46
60
  await logout()
47
61
  resetErrorBoundary()
@@ -55,6 +69,13 @@ export function LoginError({error, resetErrorBoundary}: LoginErrorProps): React.
55
69
  const description = getClientErrorApiDescription(error)
56
70
  if (description) setAuthErrorMessage(description)
57
71
  setShowRetryCta(false)
72
+ /**
73
+ * Handoff to dashboard to enable the request access flow for the project.
74
+ */
75
+ fetch('dashboard/v1/auth/access/request', {
76
+ resourceType: 'project',
77
+ resourceId: projectId,
78
+ })
58
79
  } else {
59
80
  setShowRetryCta(true)
60
81
  handleRetry()
@@ -73,7 +94,7 @@ export function LoginError({error, resetErrorBoundary}: LoginErrorProps): React.
73
94
  setAuthErrorMessage(error.message)
74
95
  setShowRetryCta(true)
75
96
  }
76
- }, [authState, handleRetry, error])
97
+ }, [authState, handleRetry, error, fetch, projectId])
77
98
 
78
99
  return (
79
100
  <Error
@@ -334,9 +334,9 @@ describe('ComlinkTokenRefresh', () => {
334
334
  })
335
335
 
336
336
  describe('when in studio mode', () => {
337
- it('should not render DashboardTokenRefresh when studio mode enabled', () => {
337
+ it('should not render DashboardTokenRefresh when studio config is provided', () => {
338
338
  render(
339
- <ResourceProvider fallback={null} studioMode={{enabled: true}}>
339
+ <ResourceProvider fallback={null} studio={{}}>
340
340
  <ComlinkTokenRefreshProvider>
341
341
  <div>Test</div>
342
342
  </ComlinkTokenRefreshProvider>
@@ -4,6 +4,7 @@ import {
4
4
  AuthStateType,
5
5
  type FrameMessage,
6
6
  getIsInDashboardState,
7
+ isStudioConfig,
7
8
  type NewTokenResponseMessage,
8
9
  type RequestNewTokenMessage,
9
10
  setAuthToken,
@@ -130,9 +131,9 @@ function DashboardTokenRefresh({children}: PropsWithChildren) {
130
131
  export const ComlinkTokenRefreshProvider: React.FC<PropsWithChildren> = ({children}) => {
131
132
  const instance = useSanityInstance()
132
133
  const isInDashboard = useMemo(() => getIsInDashboardState(instance).getCurrent(), [instance])
133
- const studioModeEnabled = !!instance.config.studioMode?.enabled
134
+ const isStudio = isStudioConfig(instance.config)
134
135
 
135
- if (isInDashboard && !studioModeEnabled) {
136
+ if (isInDashboard && !isStudio) {
136
137
  return <DashboardTokenRefresh>{children}</DashboardTokenRefresh>
137
138
  }
138
139
 
@@ -0,0 +1,126 @@
1
+ import {type SanityConfig} from '@sanity/sdk'
2
+ import {render} from '@testing-library/react'
3
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
4
+
5
+ import {SanityApp} from '../components/SanityApp'
6
+ import {SDKStudioContext, type StudioWorkspaceHandle} from './SDKStudioContext'
7
+
8
+ // Mock SDKProvider to capture the config it receives
9
+ const mockSDKProvider = vi.hoisted(() => vi.fn())
10
+ vi.mock('../components/SDKProvider', () => ({
11
+ SDKProvider: mockSDKProvider.mockImplementation(({children}) => (
12
+ <div data-testid="sdk-provider">{children}</div>
13
+ )),
14
+ }))
15
+
16
+ // Mock utils to prevent redirect logic
17
+ vi.mock('../components/utils', () => ({
18
+ isInIframe: () => true,
19
+ isLocalUrl: () => true,
20
+ }))
21
+
22
+ describe('SDKStudioContext', () => {
23
+ beforeEach(() => {
24
+ mockSDKProvider.mockClear()
25
+ })
26
+
27
+ const mockWorkspace: StudioWorkspaceHandle = {
28
+ projectId: 'studio-project-id',
29
+ dataset: 'production',
30
+ auth: {
31
+ token: {
32
+ subscribe: vi.fn(() => ({unsubscribe: vi.fn()})),
33
+ },
34
+ },
35
+ }
36
+
37
+ it('SanityApp derives config from SDKStudioContext when no config prop is given', () => {
38
+ render(
39
+ <SDKStudioContext.Provider value={mockWorkspace}>
40
+ <SanityApp fallback={<div>Loading</div>}>
41
+ <div>Child</div>
42
+ </SanityApp>
43
+ </SDKStudioContext.Provider>,
44
+ )
45
+
46
+ expect(mockSDKProvider).toHaveBeenCalled()
47
+ const receivedConfig = mockSDKProvider.mock.calls[0][0].config as SanityConfig
48
+ expect(receivedConfig).toMatchObject({
49
+ projectId: 'studio-project-id',
50
+ dataset: 'production',
51
+ studio: {
52
+ auth: {token: mockWorkspace.auth.token},
53
+ },
54
+ })
55
+ })
56
+
57
+ it('explicit config takes precedence over SDKStudioContext', () => {
58
+ const explicitConfig: SanityConfig = {
59
+ projectId: 'explicit-project',
60
+ dataset: 'staging',
61
+ }
62
+
63
+ render(
64
+ <SDKStudioContext.Provider value={mockWorkspace}>
65
+ <SanityApp config={explicitConfig} fallback={<div>Loading</div>}>
66
+ <div>Child</div>
67
+ </SanityApp>
68
+ </SDKStudioContext.Provider>,
69
+ )
70
+
71
+ expect(mockSDKProvider).toHaveBeenCalled()
72
+ const receivedConfig = mockSDKProvider.mock.calls[0][0].config as SanityConfig
73
+ expect(receivedConfig).toMatchObject({
74
+ projectId: 'explicit-project',
75
+ dataset: 'staging',
76
+ })
77
+ // Should NOT have studio config from the context
78
+ expect(receivedConfig.studio).toBeUndefined()
79
+ })
80
+
81
+ it('SanityApp works without SDKStudioContext (standalone mode)', () => {
82
+ const standaloneConfig: SanityConfig = {
83
+ projectId: 'standalone-project',
84
+ dataset: 'production',
85
+ }
86
+
87
+ render(
88
+ <SanityApp config={standaloneConfig} fallback={<div>Loading</div>}>
89
+ <div>Child</div>
90
+ </SanityApp>,
91
+ )
92
+
93
+ expect(mockSDKProvider).toHaveBeenCalled()
94
+ const receivedConfig = mockSDKProvider.mock.calls[0][0].config as SanityConfig
95
+ expect(receivedConfig).toMatchObject({
96
+ projectId: 'standalone-project',
97
+ dataset: 'production',
98
+ })
99
+ })
100
+
101
+ it('handles workspace without auth.token (older Studio)', () => {
102
+ const olderWorkspace: StudioWorkspaceHandle = {
103
+ projectId: 'older-studio',
104
+ dataset: 'production',
105
+ auth: {},
106
+ }
107
+
108
+ render(
109
+ <SDKStudioContext.Provider value={olderWorkspace}>
110
+ <SanityApp fallback={<div>Loading</div>}>
111
+ <div>Child</div>
112
+ </SanityApp>
113
+ </SDKStudioContext.Provider>,
114
+ )
115
+
116
+ expect(mockSDKProvider).toHaveBeenCalled()
117
+ const receivedConfig = mockSDKProvider.mock.calls[0][0].config as SanityConfig
118
+ expect(receivedConfig).toMatchObject({
119
+ projectId: 'older-studio',
120
+ dataset: 'production',
121
+ })
122
+ // studio config should be present but auth.token should be undefined
123
+ expect(receivedConfig.studio).toBeDefined()
124
+ expect(receivedConfig.studio?.auth).toBeUndefined()
125
+ })
126
+ })
@@ -0,0 +1,65 @@
1
+ import {type TokenSource} from '@sanity/sdk'
2
+ import {createContext} from 'react'
3
+
4
+ /**
5
+ * Minimal duck-typed interface representing a Sanity Studio workspace.
6
+ * The Studio's `Workspace` type satisfies this naturally — no import
7
+ * dependency on the `sanity` package is required.
8
+ *
9
+ * @public
10
+ */
11
+ export interface StudioWorkspaceHandle {
12
+ /** The Sanity project ID for this workspace. */
13
+ projectId: string
14
+ /** The dataset name for this workspace. */
15
+ dataset: string
16
+ /** Authentication state for this workspace. */
17
+ auth: {
18
+ /**
19
+ * Reactive token source from the Studio's auth store.
20
+ * When present, the SDK subscribes for ongoing token sync — the Studio
21
+ * is the single authority for auth and handles token refresh.
22
+ *
23
+ * Optional because Studios before Aug 2022 may not expose it. When
24
+ * absent, the SDK falls back to localStorage/cookie discovery.
25
+ */
26
+ token?: TokenSource
27
+ }
28
+ }
29
+
30
+ /**
31
+ * React context that allows the SDK to auto-detect when it is running
32
+ * inside a Sanity Studio. The Studio provides a workspace handle via this
33
+ * context, and `SanityApp` reads it to derive `projectId`, `dataset`, and
34
+ * a reactive auth token — eliminating the need for manual configuration.
35
+ *
36
+ * @remarks
37
+ * This context is defined in `@sanity/sdk-react` and provided by the Studio.
38
+ * The Studio imports it from this package and wraps its component tree:
39
+ *
40
+ * ```tsx
41
+ * import {SDKStudioContext} from '@sanity/sdk-react'
42
+ *
43
+ * function StudioRoot({children}) {
44
+ * const workspace = useWorkspace()
45
+ * return (
46
+ * <SDKStudioContext.Provider value={workspace}>
47
+ * {children}
48
+ * </SDKStudioContext.Provider>
49
+ * )
50
+ * }
51
+ * ```
52
+ *
53
+ * When `SanityApp` is rendered inside this provider, it auto-configures
54
+ * without any `config` prop:
55
+ *
56
+ * ```tsx
57
+ * <SanityApp fallback={<Loading />}>
58
+ * <MyComponent />
59
+ * </SanityApp>
60
+ * ```
61
+ *
62
+ * @public
63
+ */
64
+ export const SDKStudioContext = createContext<StudioWorkspaceHandle | null>(null)
65
+ SDKStudioContext.displayName = 'SDKStudioContext'