@sanity/sdk-react 3.0.0-rc.0 → 3.0.0-rc.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.
- package/README.md +103 -155
- package/dist/index.d.ts +1564 -682
- package/dist/index.js +1118 -612
- package/dist/index.js.map +1 -1
- package/package.json +51 -54
- package/src/_exports/index.ts +2 -4
- package/src/_exports/sdk-react.ts +26 -17
- package/src/components/SDKProvider.test.tsx +173 -32
- package/src/components/SDKProvider.tsx +87 -28
- package/src/components/SanityApp.test.tsx +68 -12
- package/src/components/SanityApp.tsx +94 -87
- package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
- package/src/components/auth/AuthBoundary.test.tsx +26 -8
- package/src/components/auth/AuthBoundary.tsx +28 -6
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +103 -59
- package/src/components/errors/ChunkLoadError.test.tsx +59 -0
- package/src/components/errors/ChunkLoadError.tsx +56 -0
- package/src/components/errors/CorsErrorComponent.tsx +2 -2
- package/src/components/errors/chunkReloadStorage.ts +57 -0
- package/src/config/handles.ts +29 -27
- package/src/constants.ts +5 -0
- package/src/context/ComlinkTokenRefresh.test.tsx +10 -27
- package/src/context/DefaultResourceContext.ts +3 -3
- package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
- package/src/context/OrganizationResourcesProvider.tsx +111 -0
- package/src/context/PerspectiveContext.ts +3 -3
- package/src/context/ProjectContext.ts +15 -0
- package/src/context/ResourceProvider.test.tsx +72 -90
- package/src/context/ResourceProvider.tsx +59 -93
- package/src/context/SDKStudioContext.test.tsx +28 -33
- package/src/context/SanityInstanceProvider.test.tsx +100 -0
- package/src/context/SanityInstanceProvider.tsx +71 -0
- package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
- package/src/context/WorkbenchTokenRefresh.tsx +61 -0
- package/src/context/renderSanityApp.test.tsx +151 -49
- package/src/context/renderSanityApp.tsx +12 -8
- package/src/context/workbenchToken.ts +63 -0
- package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
- package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
- package/src/hooks/access/useCheckPermissions.ts +24 -0
- package/src/hooks/agent/agentActions.test.tsx +1 -1
- package/src/hooks/agent/agentActions.ts +51 -71
- package/src/hooks/applications/useApplication.test-d.ts +30 -0
- package/src/hooks/applications/useApplication.ts +22 -0
- package/src/hooks/applications/useApplications.test-d.ts +31 -0
- package/src/hooks/applications/useApplications.ts +25 -0
- package/src/hooks/applications/useCreateUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useCreateUserApplication.ts +11 -0
- package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
- package/src/hooks/applications/useDeleteApplication.ts +11 -0
- package/src/hooks/applications/useDeleteUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useDeleteUserApplication.ts +11 -0
- package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
- package/src/hooks/applications/useUpdateApplication.ts +11 -0
- package/src/hooks/applications/useUpdateUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useUpdateUserApplication.ts +11 -0
- package/src/hooks/applications/useUserApplication.test-d.ts +11 -0
- package/src/hooks/applications/useUserApplication.ts +14 -0
- package/src/hooks/applications/useUserApplications.test-d.ts +11 -0
- package/src/hooks/applications/useUserApplications.ts +14 -0
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +2 -8
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +8 -32
- package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
- package/src/hooks/client/useClient.test.tsx +1 -4
- package/src/hooks/client/useClient.ts +1 -0
- package/src/hooks/context/useResource.test.tsx +32 -0
- package/src/hooks/context/useResource.ts +24 -0
- package/src/hooks/context/useSanityInstance.test.tsx +72 -3
- package/src/hooks/context/useSanityInstance.ts +30 -8
- package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
- package/src/hooks/dashboard/useManageFavorite.ts +10 -16
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +5 -7
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -6
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +0 -2
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +1 -2
- package/src/hooks/dashboard/useWindowTitle.test.ts +244 -0
- package/src/hooks/dashboard/useWindowTitle.ts +114 -0
- package/src/hooks/datasets/useDatasets.test.tsx +116 -0
- package/src/hooks/datasets/useDatasets.ts +33 -13
- package/src/hooks/document/useApplyDocumentActions.test.tsx +48 -0
- package/src/hooks/document/useApplyDocumentActions.ts +66 -48
- package/src/hooks/document/useCreateDocument.test.tsx +83 -0
- package/src/hooks/document/useCreateDocument.ts +117 -0
- package/src/hooks/document/useDocument.test.tsx +37 -8
- package/src/hooks/document/useDocument.ts +131 -66
- package/src/hooks/document/useDocumentEvent.test.tsx +16 -4
- package/src/hooks/document/useDocumentEvent.ts +16 -10
- package/src/hooks/document/useDocumentPermissions.test.tsx +8 -7
- package/src/hooks/document/useDocumentPermissions.ts +71 -39
- package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +13 -9
- package/src/hooks/document/useEditDocument.test.tsx +21 -14
- package/src/hooks/document/useEditDocument.ts +163 -17
- package/src/hooks/documents/useDocuments.test.tsx +64 -44
- package/src/hooks/documents/useDocuments.ts +36 -20
- package/src/hooks/helpers/createCallbackHook.test.tsx +13 -19
- package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
- package/src/hooks/helpers/createFetcherHook.ts +69 -0
- package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
- package/src/hooks/helpers/createMutationHook.tsx +93 -0
- package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
- package/src/hooks/helpers/createStateSourceHook.tsx +2 -1
- package/src/hooks/{document/useApplyDocumentActions.test.ts → helpers/useApplyActions.test.tsx} +92 -53
- package/src/hooks/helpers/useApplyActions.ts +68 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +92 -50
- package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
- package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
- package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
- package/src/hooks/installations/useInstallation.test-d.ts +19 -0
- package/src/hooks/installations/useInstallation.ts +22 -0
- package/src/hooks/installations/useInstallations.test-d.ts +26 -0
- package/src/hooks/installations/useInstallations.ts +25 -0
- package/src/hooks/organizations/useOrganization.test-d.ts +60 -0
- package/src/hooks/organizations/useOrganization.test.ts +63 -0
- package/src/hooks/organizations/useOrganization.ts +34 -0
- package/src/hooks/organizations/useOrganizations.test-d.ts +68 -0
- package/src/hooks/organizations/useOrganizations.test.ts +63 -0
- package/src/hooks/organizations/useOrganizations.ts +39 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +34 -27
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +40 -22
- package/src/hooks/presence/usePresence.test.tsx +0 -15
- package/src/hooks/presence/usePresence.ts +9 -20
- package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
- package/src/hooks/preview/useDocumentPreview.tsx +10 -8
- package/src/hooks/projection/useDocumentProjection.test.tsx +78 -9
- package/src/hooks/projection/useDocumentProjection.ts +81 -2
- package/src/hooks/projects/useProject.test-d.ts +56 -0
- package/src/hooks/projects/useProject.test.tsx +120 -0
- package/src/hooks/projects/useProject.ts +42 -43
- package/src/hooks/projects/useProjects.test-d.ts +60 -0
- package/src/hooks/projects/useProjects.test.ts +45 -98
- package/src/hooks/projects/useProjects.ts +19 -26
- package/src/hooks/query/useQuery.test.tsx +6 -5
- package/src/hooks/query/useQuery.ts +91 -9
- package/src/hooks/releases/useActiveReleases.test.tsx +13 -7
- package/src/hooks/releases/useActiveReleases.ts +28 -24
- package/src/hooks/releases/useAllReleases.test.tsx +93 -0
- package/src/hooks/releases/useAllReleases.ts +62 -0
- package/src/hooks/releases/useApplyReleaseActions.test.tsx +66 -0
- package/src/hooks/releases/useApplyReleaseActions.ts +82 -0
- package/src/hooks/releases/usePerspective.test.tsx +18 -10
- package/src/hooks/releases/usePerspective.ts +13 -17
- package/src/hooks/users/useUser.test.tsx +3 -9
- package/src/hooks/users/useUser.ts +2 -0
- package/src/hooks/users/useUsers.test.tsx +103 -7
- package/src/hooks/users/useUsers.ts +37 -3
- package/src/utils/resolveOrgResources.test.ts +111 -0
- package/src/utils/resolveOrgResources.ts +69 -0
- package/src/hooks/context/useDefaultResource.test.tsx +0 -25
- package/src/hooks/context/useDefaultResource.ts +0 -30
- package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -254
- package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -107
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -39
- package/src/hooks/datasets/useDatasets.test.ts +0 -66
- package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +0 -65
- package/src/hooks/projects/useProject.test.ts +0 -79
|
@@ -58,11 +58,14 @@ describe('SanityApp', () => {
|
|
|
58
58
|
mockSDKProviderComponent.mockClear()
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
-
it('renders SDKProvider with a single
|
|
62
|
-
const
|
|
61
|
+
it('renders SDKProvider with a single config', () => {
|
|
62
|
+
const singleConfig = {
|
|
63
|
+
projectId: 'test-project',
|
|
64
|
+
dataset: 'production',
|
|
65
|
+
}
|
|
63
66
|
|
|
64
67
|
render(
|
|
65
|
-
<SanityApp
|
|
68
|
+
<SanityApp config={singleConfig} fallback={<div>Loading...</div>}>
|
|
66
69
|
<div>Child Content</div>
|
|
67
70
|
</SanityApp>,
|
|
68
71
|
)
|
|
@@ -77,17 +80,60 @@ describe('SanityApp', () => {
|
|
|
77
80
|
expect(firstCallArgs1).toBeDefined()
|
|
78
81
|
expect(firstCallArgs1.length).toBeGreaterThan(0)
|
|
79
82
|
const props = firstCallArgs1[0] as unknown as SDKProviderProps
|
|
83
|
+
const config = props?.config
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
// Config is now passed directly as an object for single configs
|
|
86
|
+
expect(config).toEqual(singleConfig)
|
|
82
87
|
expect(props.fallback).toBeTruthy()
|
|
83
88
|
})
|
|
84
89
|
|
|
90
|
+
it('renders SDKProvider with multiple configs in original order', () => {
|
|
91
|
+
const multipleConfigs = [
|
|
92
|
+
{
|
|
93
|
+
projectId: 'project-1',
|
|
94
|
+
dataset: 'production',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
projectId: 'project-2',
|
|
98
|
+
dataset: 'staging',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
projectId: 'project-3',
|
|
102
|
+
dataset: 'development',
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
render(
|
|
107
|
+
<SanityApp config={multipleConfigs} fallback={<div>Loading...</div>}>
|
|
108
|
+
<div>Child Content</div>
|
|
109
|
+
</SanityApp>,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// Check that the SDKProvider is rendered
|
|
113
|
+
expect(screen.getByTestId('sdk-provider')).toBeInTheDocument()
|
|
114
|
+
|
|
115
|
+
// Verify SDKProvider was called with the correct props
|
|
116
|
+
expect(mockSDKProviderComponent).toHaveBeenCalledTimes(1)
|
|
117
|
+
const sdkProviderCalls = mockSDKProviderComponent.mock.calls
|
|
118
|
+
const firstCallArgs2 = sdkProviderCalls[0]
|
|
119
|
+
expect(firstCallArgs2).toBeDefined()
|
|
120
|
+
expect(firstCallArgs2.length).toBeGreaterThan(0)
|
|
121
|
+
const props = firstCallArgs2[0] as unknown as SDKProviderProps
|
|
122
|
+
const config = props?.config
|
|
123
|
+
|
|
124
|
+
// Config should be passed directly to SDKProvider
|
|
125
|
+
expect(config).toEqual(multipleConfigs)
|
|
126
|
+
})
|
|
127
|
+
|
|
85
128
|
it('handles iframe environment correctly', async () => {
|
|
86
129
|
// Mock window.self and window.top to simulate iframe environment
|
|
87
130
|
const originalTop = window.top
|
|
88
131
|
const originalSelf = window.self
|
|
89
132
|
|
|
90
|
-
const mockSanityConfig: SanityConfig = {
|
|
133
|
+
const mockSanityConfig: SanityConfig = {
|
|
134
|
+
projectId: 'test-project',
|
|
135
|
+
dataset: 'test-dataset',
|
|
136
|
+
}
|
|
91
137
|
|
|
92
138
|
const mockTop = {}
|
|
93
139
|
Object.defineProperty(window, 'top', {
|
|
@@ -100,7 +146,7 @@ describe('SanityApp', () => {
|
|
|
100
146
|
})
|
|
101
147
|
|
|
102
148
|
render(
|
|
103
|
-
<SanityApp config={mockSanityConfig} fallback={<div>Fallback</div>}>
|
|
149
|
+
<SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
104
150
|
<div>Test Child</div>
|
|
105
151
|
</SanityApp>,
|
|
106
152
|
)
|
|
@@ -131,7 +177,10 @@ describe('SanityApp', () => {
|
|
|
131
177
|
href: 'http://sanity-test.app',
|
|
132
178
|
}
|
|
133
179
|
|
|
134
|
-
const mockSanityConfig: SanityConfig = {
|
|
180
|
+
const mockSanityConfig: SanityConfig = {
|
|
181
|
+
projectId: 'test-project',
|
|
182
|
+
dataset: 'test-dataset',
|
|
183
|
+
}
|
|
135
184
|
|
|
136
185
|
Object.defineProperty(window, 'location', {
|
|
137
186
|
value: mockLocation,
|
|
@@ -139,7 +188,7 @@ describe('SanityApp', () => {
|
|
|
139
188
|
})
|
|
140
189
|
|
|
141
190
|
render(
|
|
142
|
-
<SanityApp config={mockSanityConfig} fallback={<div>Fallback</div>}>
|
|
191
|
+
<SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
143
192
|
<div>Test Child</div>
|
|
144
193
|
</SanityApp>,
|
|
145
194
|
)
|
|
@@ -192,7 +241,10 @@ describe('SanityApp', () => {
|
|
|
192
241
|
it('does not redirect to core if not inside iframe and local url', async () => {
|
|
193
242
|
const originalLocation = window.location
|
|
194
243
|
|
|
195
|
-
const mockSanityConfig: SanityConfig = {
|
|
244
|
+
const mockSanityConfig: SanityConfig = {
|
|
245
|
+
projectId: 'test-project',
|
|
246
|
+
dataset: 'test-dataset',
|
|
247
|
+
}
|
|
196
248
|
|
|
197
249
|
const mockLocation = {
|
|
198
250
|
replace: vi.fn(),
|
|
@@ -205,7 +257,7 @@ describe('SanityApp', () => {
|
|
|
205
257
|
})
|
|
206
258
|
|
|
207
259
|
render(
|
|
208
|
-
<SanityApp config={mockSanityConfig} fallback={<div>Fallback</div>}>
|
|
260
|
+
<SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
209
261
|
<div>Test Child</div>
|
|
210
262
|
</SanityApp>,
|
|
211
263
|
)
|
|
@@ -233,6 +285,8 @@ describe('SanityApp', () => {
|
|
|
233
285
|
}
|
|
234
286
|
|
|
235
287
|
const mockSanityConfig: SanityConfig = {
|
|
288
|
+
projectId: 'test-project',
|
|
289
|
+
dataset: 'test-dataset',
|
|
236
290
|
studio: {},
|
|
237
291
|
}
|
|
238
292
|
|
|
@@ -242,7 +296,7 @@ describe('SanityApp', () => {
|
|
|
242
296
|
})
|
|
243
297
|
|
|
244
298
|
render(
|
|
245
|
-
<SanityApp config={mockSanityConfig} fallback={<div>Fallback</div>}>
|
|
299
|
+
<SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
246
300
|
<div>Test Child</div>
|
|
247
301
|
</SanityApp>,
|
|
248
302
|
)
|
|
@@ -271,6 +325,8 @@ describe('SanityApp', () => {
|
|
|
271
325
|
}
|
|
272
326
|
|
|
273
327
|
const mockSanityConfig: SanityConfig = {
|
|
328
|
+
projectId: 'test-project',
|
|
329
|
+
dataset: 'test-dataset',
|
|
274
330
|
studio: {},
|
|
275
331
|
}
|
|
276
332
|
|
|
@@ -280,7 +336,7 @@ describe('SanityApp', () => {
|
|
|
280
336
|
})
|
|
281
337
|
|
|
282
338
|
render(
|
|
283
|
-
<SanityApp config={mockSanityConfig} fallback={<div>Fallback</div>}>
|
|
339
|
+
<SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
284
340
|
<div>Test Child</div>
|
|
285
341
|
</SanityApp>,
|
|
286
342
|
)
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_RESOURCE_NAME,
|
|
3
|
-
type DocumentResource,
|
|
4
|
-
isStudioConfig,
|
|
5
|
-
type SanityConfig,
|
|
6
|
-
} from '@sanity/sdk'
|
|
1
|
+
import {type DocumentResource, isStudioConfig, type SanityConfig} from '@sanity/sdk'
|
|
7
2
|
import {type ReactElement, useContext, useEffect, useMemo} from 'react'
|
|
8
3
|
|
|
9
4
|
import {SDKStudioContext, type StudioWorkspaceHandle} from '../context/SDKStudioContext'
|
|
@@ -16,41 +11,63 @@ import {isInIframe, isLocalUrl} from './utils'
|
|
|
16
11
|
*/
|
|
17
12
|
export interface SanityAppProps {
|
|
18
13
|
/**
|
|
19
|
-
*
|
|
20
|
-
* Optional when `SanityApp` is rendered inside an `SDKStudioContext`
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*/
|
|
24
|
-
config?: SanityConfig
|
|
25
|
-
/**
|
|
26
|
-
* Named document resources for the application. The resource keyed `"default"`
|
|
27
|
-
* is used automatically when no explicit resource is specified in hooks.
|
|
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.
|
|
28
18
|
*/
|
|
19
|
+
config?: SanityConfig | SanityConfig[]
|
|
20
|
+
/** @deprecated use the `config` prop instead. */
|
|
21
|
+
sanityConfigs?: SanityConfig[]
|
|
29
22
|
resources?: Record<string, DocumentResource>
|
|
30
23
|
children: React.ReactNode
|
|
31
24
|
/* Fallback content to show when child components are suspending. Same as the `fallback` prop for React Suspense. */
|
|
32
25
|
fallback: React.ReactNode
|
|
26
|
+
/**
|
|
27
|
+
* Set this to automatically fetch and register the organization's media library and canvas as named resources.
|
|
28
|
+
* These resources will be available to hooks as `media-library` and `canvas`.
|
|
29
|
+
*
|
|
30
|
+
* The SDK App must be running in the organization's Dashboard to use this feature.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
*
|
|
35
|
+
* const MyApp = () => {
|
|
36
|
+
* // should "just work" because of inferMediaLibraryAndCanvas.
|
|
37
|
+
* const {data: assets} = useDocuments({
|
|
38
|
+
* documentType: 'sanity.asset',
|
|
39
|
+
* resourceName: 'media-library',
|
|
40
|
+
* })
|
|
41
|
+
* return (
|
|
42
|
+
* <div>
|
|
43
|
+
* {assets.map((asset) => (
|
|
44
|
+
* <div key={asset._id}>{asset.originalFilename}</div>
|
|
45
|
+
* ))}
|
|
46
|
+
* </div>
|
|
47
|
+
* )
|
|
48
|
+
* }
|
|
49
|
+
* <SanityApp inferMediaLibraryAndCanvas>
|
|
50
|
+
* <MyApp />
|
|
51
|
+
* </SanityApp>
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
inferMediaLibraryAndCanvas?: boolean
|
|
33
55
|
}
|
|
34
56
|
|
|
35
57
|
const REDIRECT_URL = 'https://sanity.io/welcome'
|
|
36
58
|
|
|
37
59
|
/**
|
|
38
|
-
* Derive a SanityConfig
|
|
60
|
+
* Derive a SanityConfig from a Studio workspace handle.
|
|
61
|
+
* Maps the workspace's projectId, dataset, and reactive auth token into
|
|
62
|
+
* the SDK's config shape.
|
|
39
63
|
*/
|
|
40
|
-
function
|
|
41
|
-
config: SanityConfig
|
|
42
|
-
resources: Record<string, DocumentResource>
|
|
43
|
-
} {
|
|
64
|
+
function deriveConfigFromWorkspace(workspace: StudioWorkspaceHandle): SanityConfig {
|
|
44
65
|
return {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
resources: {
|
|
53
|
-
[DEFAULT_RESOURCE_NAME]: {projectId: workspace.projectId, dataset: workspace.dataset},
|
|
66
|
+
projectId: workspace.projectId,
|
|
67
|
+
dataset: workspace.dataset,
|
|
68
|
+
studio: {
|
|
69
|
+
authenticated: workspace.authenticated,
|
|
70
|
+
auth: workspace.auth.token ? {token: workspace.auth.token} : undefined,
|
|
54
71
|
},
|
|
55
72
|
}
|
|
56
73
|
}
|
|
@@ -62,49 +79,57 @@ function deriveFromWorkspace(workspace: StudioWorkspaceHandle): {
|
|
|
62
79
|
* as well as application context and state which is used by the Sanity React hooks. Your application
|
|
63
80
|
* must be wrapped with the SanityApp component to function properly.
|
|
64
81
|
*
|
|
65
|
-
* The `config` prop accepts a {@link SanityConfig} object
|
|
66
|
-
* one or more
|
|
82
|
+
* The `config` prop on the SanityApp component accepts either a single {@link SanityConfig} object, or an array of them.
|
|
83
|
+
* This allows your app to work with one or more of your organization's datasets.
|
|
67
84
|
*
|
|
68
|
-
* When rendered inside a Sanity Studio that provides `SDKStudioContext`, the `config`
|
|
69
|
-
*
|
|
85
|
+
* When rendered inside a Sanity Studio that provides `SDKStudioContext`, the `config` prop is
|
|
86
|
+
* optional — `SanityApp` will automatically derive `projectId`, `dataset`, and auth from the
|
|
87
|
+
* Studio workspace.
|
|
70
88
|
*
|
|
71
|
-
*
|
|
89
|
+
* @remarks
|
|
90
|
+
* When passing multiple SanityConfig objects to the `config` prop, the first configuration in the array becomes the default
|
|
91
|
+
* configuration used by the App SDK Hooks.
|
|
92
|
+
*
|
|
93
|
+
* When both `config` and `SDKStudioContext` are available, the explicit `config` takes precedence.
|
|
72
94
|
*
|
|
73
95
|
* @category Components
|
|
74
96
|
* @param props - Your Sanity configuration and the React children to render
|
|
75
97
|
* @returns Your Sanity application, integrated with your Sanity configuration and application context
|
|
76
98
|
*
|
|
77
|
-
* @example
|
|
99
|
+
* @example
|
|
78
100
|
* ```tsx
|
|
79
|
-
* import { SanityApp } from '@sanity/sdk-react'
|
|
101
|
+
* import { SanityApp, type SanityConfig } from '@sanity/sdk-react'
|
|
80
102
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* fallback={<div>Loading…</div>}
|
|
88
|
-
* >
|
|
89
|
-
* <MyAppRoot />
|
|
90
|
-
* </SanityApp>
|
|
91
|
-
* )
|
|
103
|
+
* import MyAppRoot from './Root'
|
|
104
|
+
*
|
|
105
|
+
* // Single project configuration
|
|
106
|
+
* const mySanityConfig: SanityConfig = {
|
|
107
|
+
* projectId: 'my-project-id',
|
|
108
|
+
* dataset: 'production',
|
|
92
109
|
* }
|
|
93
|
-
* ```
|
|
94
110
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
111
|
+
* // Or multiple project configurations
|
|
112
|
+
* const multipleConfigs: SanityConfig[] = [
|
|
113
|
+
* // Configuration for your main project. This will be used as the default project for hooks.
|
|
114
|
+
* {
|
|
115
|
+
* projectId: 'marketing-website-project',
|
|
116
|
+
* dataset: 'production',
|
|
117
|
+
* },
|
|
118
|
+
* // Configuration for a separate blog project
|
|
119
|
+
* {
|
|
120
|
+
* projectId: 'blog-project',
|
|
121
|
+
* dataset: 'production',
|
|
122
|
+
* },
|
|
123
|
+
* // Configuration for a separate ecommerce project
|
|
124
|
+
* {
|
|
125
|
+
* projectId: 'ecommerce-project',
|
|
126
|
+
* dataset: 'production',
|
|
127
|
+
* }
|
|
128
|
+
* ]
|
|
98
129
|
*
|
|
99
130
|
* export default function MyApp() {
|
|
100
131
|
* return (
|
|
101
|
-
* <SanityApp
|
|
102
|
-
* resources={{
|
|
103
|
-
* default: { projectId: 'abc123', dataset: 'production' },
|
|
104
|
-
* 'blog-project': { projectId: 'def456', dataset: 'production' },
|
|
105
|
-
* }}
|
|
106
|
-
* fallback={<div>Loading…</div>}
|
|
107
|
-
* >
|
|
132
|
+
* <SanityApp config={mySanityConfig} fallback={<div>Loading…</div>}>
|
|
108
133
|
* <MyAppRoot />
|
|
109
134
|
* </SanityApp>
|
|
110
135
|
* )
|
|
@@ -115,42 +140,29 @@ export function SanityApp({
|
|
|
115
140
|
children,
|
|
116
141
|
fallback,
|
|
117
142
|
config: configProp,
|
|
118
|
-
resources: resourcesProp,
|
|
119
143
|
...props
|
|
120
144
|
}: SanityAppProps): ReactElement {
|
|
121
145
|
const studioWorkspace = useContext(SDKStudioContext)
|
|
122
146
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return
|
|
128
|
-
}, [configProp,
|
|
129
|
-
|
|
130
|
-
const resolvedConfig = useMemo<SanityConfig>(() => {
|
|
131
|
-
if (configProp) {
|
|
132
|
-
return configProp
|
|
133
|
-
}
|
|
134
|
-
if (derived) return derived.config
|
|
135
|
-
return {}
|
|
136
|
-
}, [configProp, derived])
|
|
137
|
-
|
|
138
|
-
const resolvedResources = useMemo<Record<string, DocumentResource>>(() => {
|
|
139
|
-
if (resourcesProp) return resourcesProp
|
|
140
|
-
if (derived) return derived.resources
|
|
141
|
-
return {}
|
|
142
|
-
}, [resourcesProp, derived])
|
|
147
|
+
// Derive config: explicit config takes precedence, then Studio context
|
|
148
|
+
const resolvedConfig = useMemo(() => {
|
|
149
|
+
if (configProp) return configProp
|
|
150
|
+
if (studioWorkspace) return deriveConfigFromWorkspace(studioWorkspace)
|
|
151
|
+
return []
|
|
152
|
+
}, [configProp, studioWorkspace])
|
|
143
153
|
|
|
144
154
|
useEffect(() => {
|
|
145
155
|
let timeout: NodeJS.Timeout | undefined
|
|
156
|
+
const primaryConfig = Array.isArray(resolvedConfig) ? resolvedConfig[0] : resolvedConfig
|
|
146
157
|
const shouldRedirectWithoutConfig =
|
|
147
|
-
configProp === undefined && !studioWorkspace && !
|
|
158
|
+
configProp === undefined && !studioWorkspace && !primaryConfig
|
|
148
159
|
|
|
149
160
|
if (
|
|
150
161
|
!isInIframe() &&
|
|
151
162
|
!isLocalUrl(window) &&
|
|
152
|
-
(shouldRedirectWithoutConfig || (!!
|
|
163
|
+
(shouldRedirectWithoutConfig || (!!primaryConfig && !isStudioConfig(primaryConfig)))
|
|
153
164
|
) {
|
|
165
|
+
// If the app is not running in an iframe and is not a local url, redirect to core.
|
|
154
166
|
timeout = setTimeout(() => {
|
|
155
167
|
// eslint-disable-next-line no-console
|
|
156
168
|
console.warn('Redirecting to core', REDIRECT_URL)
|
|
@@ -161,12 +173,7 @@ export function SanityApp({
|
|
|
161
173
|
}, [configProp, resolvedConfig, studioWorkspace])
|
|
162
174
|
|
|
163
175
|
return (
|
|
164
|
-
<SDKProvider
|
|
165
|
-
{...props}
|
|
166
|
-
fallback={fallback}
|
|
167
|
-
config={resolvedConfig}
|
|
168
|
-
resources={resolvedResources}
|
|
169
|
-
>
|
|
176
|
+
<SDKProvider {...props} fallback={fallback} config={resolvedConfig}>
|
|
170
177
|
{children}
|
|
171
178
|
</SDKProvider>
|
|
172
179
|
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {AuthStateType, getIsInDashboardState} from '@sanity/sdk'
|
|
2
|
+
import {render, screen, waitFor} from '@testing-library/react'
|
|
3
|
+
import {beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
6
|
+
import {useAuthState} from '../../hooks/auth/useAuthState'
|
|
7
|
+
import {AuthBoundary} from './AuthBoundary'
|
|
8
|
+
|
|
9
|
+
// NOTE: unlike AuthBoundary.test.tsx this file does NOT mock react-error-boundary
|
|
10
|
+
// — we want the REAL ErrorBoundary so we can observe whether it recovers when the
|
|
11
|
+
// auth store transitions back to LOGGED_IN (e.g. after a successful silent token
|
|
12
|
+
// refresh via ComlinkTokenRefresh -> setAuthToken).
|
|
13
|
+
|
|
14
|
+
vi.mock('@sanity/sdk', async () => {
|
|
15
|
+
const actual = await vi.importActual('@sanity/sdk')
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
getIsInDashboardState: vi.fn(() => ({getCurrent: () => true})),
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
vi.mock('../../hooks/auth/useAuthState', () => ({useAuthState: vi.fn()}))
|
|
23
|
+
vi.mock('../../hooks/auth/useLoginUrl', () => ({
|
|
24
|
+
useLoginUrl: vi.fn(() => 'https://example.com/login'),
|
|
25
|
+
}))
|
|
26
|
+
vi.mock('../../hooks/auth/useVerifyOrgProjects', () => ({useVerifyOrgProjects: vi.fn(() => null)}))
|
|
27
|
+
vi.mock('../../hooks/auth/useLogOut', () => ({useLogOut: vi.fn(() => async () => {})}))
|
|
28
|
+
vi.mock('../../hooks/auth/useHandleAuthCallback', () => ({
|
|
29
|
+
useHandleAuthCallback: vi.fn(() => async () => {}),
|
|
30
|
+
}))
|
|
31
|
+
vi.mock('../../hooks/comlink/useWindowConnection', () => ({
|
|
32
|
+
useWindowConnection: vi.fn(() => ({fetch: vi.fn().mockResolvedValue({token: 'fresh-token'})})),
|
|
33
|
+
}))
|
|
34
|
+
// Avoid injecting the SanityOS bridge.js <script> during import.
|
|
35
|
+
vi.mock('../utils', () => ({isInIframe: vi.fn(() => false)}))
|
|
36
|
+
|
|
37
|
+
const mockUseAuthState = useAuthState as Mock
|
|
38
|
+
|
|
39
|
+
describe('AuthBoundary — recovery after silent token refresh (dashboard)', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks()
|
|
42
|
+
;(getIsInDashboardState as Mock).mockReturnValue({getCurrent: () => true})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('renders the app again once the session is re-established (LOGGED_IN)', async () => {
|
|
46
|
+
// 1. Session expires: a request 401s and the auth store goes to ERROR.
|
|
47
|
+
mockUseAuthState.mockReturnValue({
|
|
48
|
+
type: AuthStateType.ERROR,
|
|
49
|
+
error: Object.assign(new Error('Unauthorized'), {statusCode: 401}),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const {rerender} = render(
|
|
53
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
54
|
+
<AuthBoundary projectIds={['p']}>
|
|
55
|
+
<div>Protected Content</div>
|
|
56
|
+
</AuthBoundary>
|
|
57
|
+
</ResourceProvider>,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// The error boundary catches the AuthError and shows the auth-error screen.
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(screen.getByText('Authentication Error')).toBeInTheDocument()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// 2. ComlinkTokenRefresh fetches a fresh token from the Dashboard, setAuthToken
|
|
66
|
+
// lands it, /users/me re-fetches and the store returns to LOGGED_IN.
|
|
67
|
+
mockUseAuthState.mockReturnValue({
|
|
68
|
+
type: AuthStateType.LOGGED_IN,
|
|
69
|
+
currentUser: null,
|
|
70
|
+
token: 'fresh-token',
|
|
71
|
+
})
|
|
72
|
+
rerender(
|
|
73
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
74
|
+
<AuthBoundary projectIds={['p']}>
|
|
75
|
+
<div>Protected Content</div>
|
|
76
|
+
</AuthBoundary>
|
|
77
|
+
</ResourceProvider>,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
// EXPECTED (desired) behaviour: the app recovers automatically, no Retry click.
|
|
81
|
+
await waitFor(() => {
|
|
82
|
+
expect(screen.getByText('Protected Content')).toBeInTheDocument()
|
|
83
|
+
})
|
|
84
|
+
expect(screen.queryByText('Authentication Error')).not.toBeInTheDocument()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {AuthStateType} from '@sanity/sdk'
|
|
1
|
+
import {AuthStateType, type SanityConfig} from '@sanity/sdk'
|
|
2
2
|
import {render, screen, waitFor} from '@testing-library/react'
|
|
3
3
|
import React from 'react'
|
|
4
4
|
import {type FallbackProps} from 'react-error-boundary'
|
|
@@ -110,6 +110,24 @@ describe('AuthBoundary', () => {
|
|
|
110
110
|
const mockUseVerifyOrgProjects = vi.mocked(useVerifyOrgProjects)
|
|
111
111
|
const testProjectIds = ['proj-test'] // Example project ID for tests
|
|
112
112
|
|
|
113
|
+
// Mock Sanity instance
|
|
114
|
+
const mockSanityInstance = {
|
|
115
|
+
instanceId: 'test-instance-id',
|
|
116
|
+
config: {
|
|
117
|
+
projectId: 'test-project',
|
|
118
|
+
dataset: 'test-dataset',
|
|
119
|
+
},
|
|
120
|
+
isDisposed: () => false,
|
|
121
|
+
dispose: () => {},
|
|
122
|
+
onDispose: () => () => {},
|
|
123
|
+
getParent: () => undefined,
|
|
124
|
+
createChild: (config: SanityConfig) => ({
|
|
125
|
+
...mockSanityInstance,
|
|
126
|
+
config: {...mockSanityInstance.config, ...config},
|
|
127
|
+
}),
|
|
128
|
+
match: () => undefined,
|
|
129
|
+
}
|
|
130
|
+
|
|
113
131
|
beforeEach(() => {
|
|
114
132
|
vi.clearAllMocks()
|
|
115
133
|
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
@@ -148,7 +166,7 @@ describe('AuthBoundary', () => {
|
|
|
148
166
|
isExchangingToken: false,
|
|
149
167
|
})
|
|
150
168
|
const {container} = render(
|
|
151
|
-
<ResourceProvider
|
|
169
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
152
170
|
<AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
|
|
153
171
|
</ResourceProvider>,
|
|
154
172
|
)
|
|
@@ -165,7 +183,7 @@ describe('AuthBoundary', () => {
|
|
|
165
183
|
token: 'exampleToken',
|
|
166
184
|
})
|
|
167
185
|
render(
|
|
168
|
-
<ResourceProvider
|
|
186
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
169
187
|
<AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
|
|
170
188
|
</ResourceProvider>,
|
|
171
189
|
)
|
|
@@ -179,7 +197,7 @@ describe('AuthBoundary', () => {
|
|
|
179
197
|
error: new Error('test error'),
|
|
180
198
|
})
|
|
181
199
|
render(
|
|
182
|
-
<ResourceProvider
|
|
200
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
183
201
|
<AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
|
|
184
202
|
</ResourceProvider>,
|
|
185
203
|
)
|
|
@@ -196,7 +214,7 @@ describe('AuthBoundary', () => {
|
|
|
196
214
|
|
|
197
215
|
it('renders children when logged in and org verification passes', () => {
|
|
198
216
|
render(
|
|
199
|
-
<ResourceProvider
|
|
217
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
200
218
|
<AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
|
|
201
219
|
</ResourceProvider>,
|
|
202
220
|
)
|
|
@@ -218,7 +236,7 @@ describe('AuthBoundary', () => {
|
|
|
218
236
|
|
|
219
237
|
// Need to catch the error thrown during render. ErrorBoundary mock handles this.
|
|
220
238
|
render(
|
|
221
|
-
<ResourceProvider
|
|
239
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
222
240
|
<AuthBoundary verifyOrganization={true} projectIds={testProjectIds}>
|
|
223
241
|
<div>Protected Content</div>
|
|
224
242
|
</AuthBoundary>
|
|
@@ -250,7 +268,7 @@ describe('AuthBoundary', () => {
|
|
|
250
268
|
})
|
|
251
269
|
|
|
252
270
|
render(
|
|
253
|
-
<ResourceProvider
|
|
271
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
254
272
|
<AuthBoundary verifyOrganization={false} projectIds={testProjectIds}>
|
|
255
273
|
<div>Protected Content</div>
|
|
256
274
|
</AuthBoundary>
|
|
@@ -275,7 +293,7 @@ describe('AuthBoundary', () => {
|
|
|
275
293
|
mockUseVerifyOrgProjects.mockImplementation(() => null)
|
|
276
294
|
|
|
277
295
|
render(
|
|
278
|
-
<ResourceProvider
|
|
296
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
279
297
|
<AuthBoundary projectIds={testProjectIds}>
|
|
280
298
|
<div>Protected Content</div>
|
|
281
299
|
</AuthBoundary>
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import {CorsOriginError} from '@sanity/client'
|
|
2
|
-
import {AuthStateType, getCorsErrorProjectId, isStudioConfig} from '@sanity/sdk'
|
|
2
|
+
import {AuthStateType, getCorsErrorProjectId, isImportError, isStudioConfig} from '@sanity/sdk'
|
|
3
3
|
import {useEffect, useMemo} from 'react'
|
|
4
4
|
import {ErrorBoundary, type FallbackProps} from 'react-error-boundary'
|
|
5
5
|
|
|
6
6
|
import {ComlinkTokenRefreshProvider} from '../../context/ComlinkTokenRefresh'
|
|
7
|
+
import {isWorkbenchEnvironment} from '../../context/workbenchToken'
|
|
8
|
+
import {WorkbenchTokenRefreshProvider} from '../../context/WorkbenchTokenRefresh'
|
|
7
9
|
import {useAuthState} from '../../hooks/auth/useAuthState'
|
|
8
10
|
import {useLoginUrl} from '../../hooks/auth/useLoginUrl'
|
|
9
11
|
import {useVerifyOrgProjects} from '../../hooks/auth/useVerifyOrgProjects'
|
|
10
12
|
import {useSanityInstance} from '../../hooks/context/useSanityInstance'
|
|
13
|
+
import {ChunkLoadError} from '../errors/ChunkLoadError'
|
|
11
14
|
import {CorsErrorComponent} from '../errors/CorsErrorComponent'
|
|
12
15
|
import {isInIframe} from '../utils'
|
|
13
16
|
import {AuthError} from './AuthError'
|
|
@@ -108,8 +111,24 @@ export function AuthBoundary({
|
|
|
108
111
|
LoginErrorComponent = LoginError,
|
|
109
112
|
...props
|
|
110
113
|
}: AuthBoundaryProps): React.ReactNode {
|
|
114
|
+
/**
|
|
115
|
+
* When the session is re-established (e.g. ComlinkTokenRefresh silently mints a
|
|
116
|
+
* fresh token via setAuthToken), the auth store returns to LOGGED_IN but the
|
|
117
|
+
* ErrorBoundary stays latched on its fallback until reset. Keying it on the
|
|
118
|
+
* recovered session lets it clear automatically
|
|
119
|
+
*/
|
|
120
|
+
const authState = useAuthState()
|
|
121
|
+
const sessionResetKey =
|
|
122
|
+
authState.type === AuthStateType.LOGGED_IN ? authState.token : authState.type
|
|
123
|
+
|
|
111
124
|
const FallbackComponent = useMemo(() => {
|
|
112
125
|
return function LoginComponentWithLayoutProps(fallbackProps: FallbackProps) {
|
|
126
|
+
// Chunk-load errors from any lazy-loaded code beneath this boundary
|
|
127
|
+
// (typically the consumer's app) get the chunk-aware fallback instead
|
|
128
|
+
// of being misreported as auth errors.
|
|
129
|
+
if (isImportError(fallbackProps.error)) {
|
|
130
|
+
return <ChunkLoadError {...fallbackProps} />
|
|
131
|
+
}
|
|
113
132
|
if (fallbackProps.error instanceof CorsOriginError) {
|
|
114
133
|
return (
|
|
115
134
|
<CorsErrorComponent
|
|
@@ -124,9 +143,11 @@ export function AuthBoundary({
|
|
|
124
143
|
|
|
125
144
|
return (
|
|
126
145
|
<ComlinkTokenRefreshProvider>
|
|
127
|
-
<
|
|
128
|
-
<
|
|
129
|
-
|
|
146
|
+
<WorkbenchTokenRefreshProvider>
|
|
147
|
+
<ErrorBoundary FallbackComponent={FallbackComponent} resetKeys={[sessionResetKey]}>
|
|
148
|
+
<AuthSwitch {...props} />
|
|
149
|
+
</ErrorBoundary>
|
|
150
|
+
</WorkbenchTokenRefreshProvider>
|
|
130
151
|
</ComlinkTokenRefreshProvider>
|
|
131
152
|
)
|
|
132
153
|
}
|
|
@@ -165,8 +186,9 @@ function AuthSwitch({
|
|
|
165
186
|
const loginUrl = useLoginUrl()
|
|
166
187
|
|
|
167
188
|
useEffect(() => {
|
|
168
|
-
if (isLoggedOut && !isInIframe() && !isStudio) {
|
|
169
|
-
// We don't want to redirect to login if we're in the Dashboard
|
|
189
|
+
if (isLoggedOut && !isInIframe() && !isStudio && !isWorkbenchEnvironment()) {
|
|
190
|
+
// We don't want to redirect to login if we're in the Dashboard, in studio
|
|
191
|
+
// mode, or in the workbench (the OS owns the session and mints the token)
|
|
170
192
|
window.location.href = loginUrl
|
|
171
193
|
}
|
|
172
194
|
}, [isLoggedOut, loginUrl, isStudio])
|