@sanity/sdk-react 0.0.0-alpha.8 → 0.0.0-chore-react-18-compat.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/README.md +33 -126
- package/dist/index.d.ts +4811 -2
- package/dist/index.js +1069 -2
- package/dist/index.js.map +1 -1
- package/package.json +23 -45
- package/src/_exports/index.ts +66 -10
- package/src/components/Login/LoginLinks.test.tsx +90 -0
- package/src/components/Login/LoginLinks.tsx +58 -0
- package/src/components/SDKProvider.test.tsx +79 -0
- package/src/components/SDKProvider.tsx +42 -0
- package/src/components/SanityApp.test.tsx +104 -2
- package/src/components/SanityApp.tsx +54 -17
- package/src/components/auth/AuthBoundary.test.tsx +4 -4
- package/src/components/auth/AuthBoundary.tsx +13 -3
- package/src/components/auth/Login.test.tsx +1 -1
- package/src/components/auth/Login.tsx +11 -26
- package/src/components/auth/LoginCallback.test.tsx +3 -3
- package/src/components/auth/LoginCallback.tsx +8 -11
- package/src/components/auth/LoginError.tsx +12 -8
- package/src/components/auth/LoginFooter.tsx +13 -20
- package/src/components/auth/LoginLayout.tsx +8 -9
- package/src/components/auth/authTestHelpers.tsx +1 -8
- package/src/components/utils.ts +22 -0
- package/src/context/SanityInstanceContext.ts +4 -0
- package/src/context/SanityProvider.test.tsx +1 -1
- package/src/context/SanityProvider.tsx +10 -8
- package/src/hooks/_synchronous-groq-js.mjs +4 -0
- package/src/hooks/auth/useAuthState.tsx +0 -2
- package/src/hooks/auth/useCurrentUser.tsx +27 -20
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
- package/src/hooks/auth/useDashboardOrganizationId.tsx +29 -0
- package/src/hooks/auth/useHandleAuthCallback.test.tsx +16 -0
- package/src/hooks/auth/{useHandleCallback.tsx → useHandleAuthCallback.tsx} +6 -6
- package/src/hooks/auth/useLogOut.test.tsx +2 -2
- package/src/hooks/client/useClient.ts +9 -30
- package/src/hooks/comlink/useFrameConnection.test.tsx +55 -10
- package/src/hooks/comlink/useFrameConnection.ts +39 -43
- package/src/hooks/comlink/useManageFavorite.test.ts +111 -0
- package/src/hooks/comlink/useManageFavorite.ts +130 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +81 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +106 -0
- package/src/hooks/comlink/useWindowConnection.test.ts +53 -12
- package/src/hooks/comlink/useWindowConnection.ts +69 -29
- package/src/hooks/context/useSanityInstance.test.tsx +1 -1
- package/src/hooks/context/useSanityInstance.ts +21 -5
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +178 -0
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +123 -0
- package/src/hooks/dashboard/useStudioWorkspacesByResourceId.test.tsx +278 -0
- package/src/hooks/dashboard/useStudioWorkspacesByResourceId.ts +92 -0
- package/src/hooks/datasets/useDatasets.ts +40 -0
- package/src/hooks/document/useApplyDocumentActions.test.ts +25 -0
- package/src/hooks/document/useApplyDocumentActions.ts +75 -0
- package/src/hooks/document/useDocument.test.ts +81 -0
- package/src/hooks/document/useDocument.ts +107 -0
- package/src/hooks/document/useDocumentEvent.test.ts +63 -0
- package/src/hooks/document/useDocumentEvent.ts +54 -0
- package/src/hooks/document/useDocumentPermissions.ts +84 -0
- package/src/hooks/document/useDocumentSyncStatus.test.ts +16 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +33 -0
- package/src/hooks/document/useEditDocument.test.ts +179 -0
- package/src/hooks/document/useEditDocument.ts +195 -0
- package/src/hooks/documents/useDocuments.test.tsx +152 -0
- package/src/hooks/documents/useDocuments.ts +174 -0
- package/src/hooks/helpers/createCallbackHook.tsx +3 -2
- package/src/hooks/helpers/createStateSourceHook.test.tsx +66 -0
- package/src/hooks/helpers/createStateSourceHook.tsx +29 -10
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +259 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +290 -0
- package/src/hooks/preview/usePreview.test.tsx +6 -6
- package/src/hooks/preview/usePreview.tsx +12 -9
- package/src/hooks/projection/useProjection.test.tsx +218 -0
- package/src/hooks/projection/useProjection.ts +147 -0
- package/src/hooks/projects/useProject.ts +48 -0
- package/src/hooks/projects/useProjects.ts +45 -0
- package/src/hooks/query/useQuery.test.tsx +188 -0
- package/src/hooks/query/useQuery.ts +103 -0
- package/src/hooks/users/useUsers.test.ts +163 -0
- package/src/hooks/users/useUsers.ts +107 -0
- package/src/utils/getEnv.ts +21 -0
- package/src/version.ts +8 -0
- package/dist/_chunks-es/context.js +0 -8
- package/dist/_chunks-es/context.js.map +0 -1
- package/dist/_chunks-es/useLogOut.js +0 -44
- package/dist/_chunks-es/useLogOut.js.map +0 -1
- package/dist/components.d.ts +0 -111
- package/dist/components.js +0 -153
- package/dist/components.js.map +0 -1
- package/dist/context.d.ts +0 -45
- package/dist/context.js +0 -5
- package/dist/context.js.map +0 -1
- package/dist/hooks.d.ts +0 -3485
- package/dist/hooks.js +0 -167
- package/dist/hooks.js.map +0 -1
- package/src/_exports/components.ts +0 -2
- package/src/_exports/context.ts +0 -2
- package/src/_exports/hooks.ts +0 -27
- package/src/hooks/auth/useHandleCallback.test.tsx +0 -16
- package/src/hooks/client/useClient.test.tsx +0 -130
- package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
- package/src/hooks/documentCollection/useDocuments.ts +0 -135
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {AuthStateType, createSanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {SanityProvider, useAuthState, useLoginUrls} from '@sanity/sdk-react'
|
|
3
|
+
import {render, screen} from '@testing-library/react'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
6
|
+
|
|
7
|
+
import {LoginLinks} from './LoginLinks'
|
|
8
|
+
|
|
9
|
+
// Mock the hooks and SDK functions
|
|
10
|
+
vi.mock('../../hooks/auth/useLoginUrls', () => ({
|
|
11
|
+
useLoginUrls: vi.fn(() => [
|
|
12
|
+
{
|
|
13
|
+
name: 'google',
|
|
14
|
+
title: 'Google',
|
|
15
|
+
url: 'https://google.com/auth',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'github',
|
|
19
|
+
title: 'GitHub',
|
|
20
|
+
url: 'https://github.com/auth',
|
|
21
|
+
},
|
|
22
|
+
]),
|
|
23
|
+
}))
|
|
24
|
+
vi.mock('@sanity/sdk', async () => {
|
|
25
|
+
const actual = await vi.importActual('@sanity/sdk')
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
...actual,
|
|
29
|
+
tradeTokenForSession: vi.fn(),
|
|
30
|
+
getSidUrlHash: vi.fn().mockReturnValue(null),
|
|
31
|
+
getSidUrlSearch: vi.fn(),
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
vi.mock('../../hooks/auth/useAuthState', () => ({
|
|
36
|
+
useAuthState: vi.fn(() => 'logged-out'),
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
vi.mock('../../hooks/auth/useHandleAuthCallback', () => ({
|
|
40
|
+
useHandleAuthCallback: vi.fn(),
|
|
41
|
+
}))
|
|
42
|
+
|
|
43
|
+
describe('LoginLinks', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.clearAllMocks()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
|
|
49
|
+
const renderWithWrappers = (ui: React.ReactElement) => {
|
|
50
|
+
return render(<SanityProvider sanityInstances={[sanityInstance]}>{ui}</SanityProvider>)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
it('renders auth provider links correctly when not authenticated', () => {
|
|
54
|
+
vi.mocked(useAuthState).mockReturnValue({
|
|
55
|
+
type: AuthStateType.LOGGED_OUT,
|
|
56
|
+
isDestroyingSession: false,
|
|
57
|
+
})
|
|
58
|
+
renderWithWrappers(<LoginLinks />)
|
|
59
|
+
|
|
60
|
+
expect(screen.getByText('Choose login provider')).toBeInTheDocument()
|
|
61
|
+
|
|
62
|
+
const authProviders = useLoginUrls()
|
|
63
|
+
authProviders.forEach((provider) => {
|
|
64
|
+
const button = screen.getByRole('link', {name: provider.title})
|
|
65
|
+
expect(button).toBeInTheDocument()
|
|
66
|
+
expect(button).toHaveAttribute('href', provider.url)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('shows loading state while logging in', () => {
|
|
71
|
+
vi.mocked(useAuthState).mockReturnValue({
|
|
72
|
+
type: AuthStateType.LOGGING_IN,
|
|
73
|
+
isExchangingToken: false,
|
|
74
|
+
})
|
|
75
|
+
renderWithWrappers(<LoginLinks />)
|
|
76
|
+
|
|
77
|
+
expect(screen.getByText('Logging in...')).toBeInTheDocument()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('shows success message when logged in', () => {
|
|
81
|
+
vi.mocked(useAuthState).mockReturnValue({
|
|
82
|
+
type: AuthStateType.LOGGED_IN,
|
|
83
|
+
token: 'test-token',
|
|
84
|
+
currentUser: null,
|
|
85
|
+
})
|
|
86
|
+
renderWithWrappers(<LoginLinks />)
|
|
87
|
+
|
|
88
|
+
expect(screen.getByText('You are logged in')).toBeInTheDocument()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {type ReactElement} from 'react'
|
|
2
|
+
|
|
3
|
+
import {useAuthState} from '../../hooks/auth/useAuthState'
|
|
4
|
+
import {useHandleAuthCallback} from '../../hooks/auth/useHandleAuthCallback'
|
|
5
|
+
import {useLoginUrls} from '../../hooks/auth/useLoginUrls'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Component that handles Sanity authentication flow and renders login provider options
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*
|
|
12
|
+
* @returns Rendered component
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* The component handles three states:
|
|
16
|
+
* 1. Loading state during token exchange
|
|
17
|
+
* 2. Success state after successful authentication
|
|
18
|
+
* 3. Provider selection UI when not authenticated
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* const config = { projectId: 'your-project-id', dataset: 'production' }
|
|
23
|
+
* return <LoginLinks sanityInstance={config} />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const LoginLinks = (): ReactElement => {
|
|
27
|
+
const loginUrls = useLoginUrls()
|
|
28
|
+
const authState = useAuthState()
|
|
29
|
+
useHandleAuthCallback()
|
|
30
|
+
|
|
31
|
+
if (authState.type === 'logging-in') {
|
|
32
|
+
return <div className="sc-login-links__logging-in">Logging in...</div>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Show success state after authentication
|
|
36
|
+
if (authState.type === 'logged-in') {
|
|
37
|
+
return <div className="sc-login-links__logged-in">You are logged in</div>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Render provider selection UI
|
|
42
|
+
*/
|
|
43
|
+
return (
|
|
44
|
+
<div className="sc-login-links">
|
|
45
|
+
<h2 className="sc-login-links__title">Choose login provider</h2>
|
|
46
|
+
|
|
47
|
+
<ul className="sc-login-links__list">
|
|
48
|
+
{loginUrls.map((provider, index) => (
|
|
49
|
+
<li key={`${provider.url}_${index}`} className="sc-login-links__item">
|
|
50
|
+
<a href={provider.url} className="sc-login-links__link">
|
|
51
|
+
{provider.title}
|
|
52
|
+
</a>
|
|
53
|
+
</li>
|
|
54
|
+
))}
|
|
55
|
+
</ul>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as SanitySDK from '@sanity/sdk'
|
|
2
|
+
import {render} from '@testing-library/react'
|
|
3
|
+
import {type ReactNode} from 'react'
|
|
4
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {type SanityProviderProps} from '../context/SanityProvider'
|
|
7
|
+
import {SDKProvider} from './SDKProvider'
|
|
8
|
+
|
|
9
|
+
// Mock the SDK module
|
|
10
|
+
vi.mock('@sanity/sdk', () => ({
|
|
11
|
+
createSanityInstance: vi.fn(() => ({
|
|
12
|
+
// Mock return value of createSanityInstance
|
|
13
|
+
id: 'mock-instance',
|
|
14
|
+
})),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
// Mock the SanityProvider context to verify the instance is passed correctly
|
|
18
|
+
vi.mock('../context/SanityProvider', () => ({
|
|
19
|
+
SanityProvider: ({children, sanityInstances}: SanityProviderProps) => (
|
|
20
|
+
<div data-testid="sanity-provider" data-instances={JSON.stringify(sanityInstances)}>
|
|
21
|
+
{children}
|
|
22
|
+
</div>
|
|
23
|
+
),
|
|
24
|
+
useSanity: vi.fn(),
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
// Mock the AuthBoundary component
|
|
28
|
+
vi.mock('./auth/AuthBoundary', () => ({
|
|
29
|
+
AuthBoundary: ({children}: {children: ReactNode}) => (
|
|
30
|
+
<div data-testid="auth-boundary">{children}</div>
|
|
31
|
+
),
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
describe('SDKProvider', () => {
|
|
35
|
+
const mockConfig = {
|
|
36
|
+
projectId: 'test-project',
|
|
37
|
+
dataset: 'test-dataset',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
it('creates a Sanity instance with the provided config', () => {
|
|
41
|
+
render(
|
|
42
|
+
<SDKProvider sanityConfigs={[mockConfig]} fallback={<div>Fallback</div>}>
|
|
43
|
+
<div>Test Child</div>
|
|
44
|
+
</SDKProvider>,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
expect(SanitySDK.createSanityInstance).toHaveBeenCalledWith(mockConfig)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('renders children within SanityProvider and AuthBoundary', () => {
|
|
51
|
+
const {getByText, getByTestId} = render(
|
|
52
|
+
<SDKProvider sanityConfigs={[mockConfig]} fallback={<div>Fallback</div>}>
|
|
53
|
+
<div>Test Child</div>
|
|
54
|
+
</SDKProvider>,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// Verify the component hierarchy
|
|
58
|
+
const sanityProvider = getByTestId('sanity-provider')
|
|
59
|
+
const authBoundary = getByTestId('auth-boundary')
|
|
60
|
+
const childElement = getByText('Test Child')
|
|
61
|
+
|
|
62
|
+
expect(sanityProvider).toBeInTheDocument()
|
|
63
|
+
expect(authBoundary).toBeInTheDocument()
|
|
64
|
+
expect(childElement).toBeInTheDocument()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('passes the created Sanity instance to SanityProvider', () => {
|
|
68
|
+
const {getByTestId} = render(
|
|
69
|
+
<SDKProvider sanityConfigs={[mockConfig]} fallback={<div>Fallback</div>}>
|
|
70
|
+
<div>Test Child</div>
|
|
71
|
+
</SDKProvider>,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const sanityProvider = getByTestId('sanity-provider')
|
|
75
|
+
const passedInstances = JSON.parse(sanityProvider.dataset['instances'] || '[]')
|
|
76
|
+
|
|
77
|
+
expect(passedInstances).toEqual([{id: 'mock-instance'}])
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {createSanityInstance, type SanityConfig} from '@sanity/sdk'
|
|
2
|
+
import {type ReactElement, type ReactNode, Suspense, useMemo} from 'react'
|
|
3
|
+
|
|
4
|
+
import {SanityProvider} from '../context/SanityProvider'
|
|
5
|
+
import {AuthBoundary} from './auth/AuthBoundary'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_FALLBACK = (
|
|
8
|
+
<>
|
|
9
|
+
Warning: No fallback provided. Please supply a fallback prop to ensure proper Suspense handling.
|
|
10
|
+
</>
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export interface SDKProviderProps {
|
|
17
|
+
children: ReactNode
|
|
18
|
+
sanityConfigs: SanityConfig[]
|
|
19
|
+
fallback: ReactNode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @internal
|
|
24
|
+
*
|
|
25
|
+
* Top-level context provider that provides access to the Sanity SDK.
|
|
26
|
+
*/
|
|
27
|
+
export function SDKProvider({children, sanityConfigs, fallback}: SDKProviderProps): ReactElement {
|
|
28
|
+
const sanityInstances = useMemo(() => {
|
|
29
|
+
return sanityConfigs.map((sanityConfig: SanityConfig) => createSanityInstance(sanityConfig))
|
|
30
|
+
}, [sanityConfigs])
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<SanityProvider sanityInstances={sanityInstances}>
|
|
34
|
+
{/* This Suspense boundary is necessary because some hooks may suspend.
|
|
35
|
+
It ensures that the Sanity instance state created above remains stable
|
|
36
|
+
before rendering the AuthBoundary and its children. */}
|
|
37
|
+
<Suspense fallback={fallback ?? DEFAULT_FALLBACK}>
|
|
38
|
+
<AuthBoundary>{children}</AuthBoundary>
|
|
39
|
+
</Suspense>
|
|
40
|
+
</SanityProvider>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {AuthStateType, type SanityConfig} from '@sanity/sdk'
|
|
2
2
|
import {render, screen} from '@testing-library/react'
|
|
3
|
-
import {describe, expect, it} from 'vitest'
|
|
3
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {SanityApp} from './SanityApp'
|
|
6
6
|
|
|
@@ -44,11 +44,113 @@ describe('SanityApp', () => {
|
|
|
44
44
|
it('renders children correctly', async () => {
|
|
45
45
|
const testMessage = 'Test Child Component'
|
|
46
46
|
render(
|
|
47
|
-
<SanityApp
|
|
47
|
+
<SanityApp sanityConfigs={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
48
48
|
<div>{testMessage}</div>
|
|
49
49
|
</SanityApp>,
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
expect(await screen.findByText(testMessage)).toBeInTheDocument()
|
|
53
53
|
})
|
|
54
|
+
|
|
55
|
+
it('handles iframe environment correctly', async () => {
|
|
56
|
+
// Mock window.self and window.top to simulate iframe environment
|
|
57
|
+
const originalTop = window.top
|
|
58
|
+
const originalSelf = window.self
|
|
59
|
+
|
|
60
|
+
const mockTop = {}
|
|
61
|
+
Object.defineProperty(window, 'top', {
|
|
62
|
+
value: mockTop,
|
|
63
|
+
writable: true,
|
|
64
|
+
})
|
|
65
|
+
Object.defineProperty(window, 'self', {
|
|
66
|
+
value: window,
|
|
67
|
+
writable: true,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
render(
|
|
71
|
+
<SanityApp sanityConfigs={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
72
|
+
<div>Test Child</div>
|
|
73
|
+
</SanityApp>,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// Wait for 1 second
|
|
77
|
+
await new Promise((resolve) => setTimeout(resolve, 1010))
|
|
78
|
+
|
|
79
|
+
// Add assertions based on your iframe-specific behavior
|
|
80
|
+
expect(window.location.href).toBe('http://localhost:3000/')
|
|
81
|
+
|
|
82
|
+
// Clean up the mock
|
|
83
|
+
Object.defineProperty(window, 'top', {
|
|
84
|
+
value: originalTop,
|
|
85
|
+
writable: true,
|
|
86
|
+
})
|
|
87
|
+
Object.defineProperty(window, 'self', {
|
|
88
|
+
value: originalSelf,
|
|
89
|
+
writable: true,
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('redirects to core if not inside iframe and not local url', async () => {
|
|
94
|
+
const originalLocation = window.location
|
|
95
|
+
|
|
96
|
+
const mockLocation = {
|
|
97
|
+
replace: vi.fn(),
|
|
98
|
+
href: 'http://sanity-test.app',
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Object.defineProperty(window, 'location', {
|
|
102
|
+
value: mockLocation,
|
|
103
|
+
writable: true,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
render(
|
|
107
|
+
<SanityApp sanityConfigs={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
108
|
+
<div>Test Child</div>
|
|
109
|
+
</SanityApp>,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// Wait for 1 second
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, 1010))
|
|
114
|
+
|
|
115
|
+
// Add assertions based on your iframe-specific behavior
|
|
116
|
+
expect(mockLocation.replace).toHaveBeenCalledWith('https://core.sanity.io')
|
|
117
|
+
|
|
118
|
+
// Clean up the mock
|
|
119
|
+
Object.defineProperty(window, 'location', {
|
|
120
|
+
value: originalLocation,
|
|
121
|
+
writable: true,
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('does not redirect to core if not inside iframe and local url', async () => {
|
|
126
|
+
const originalLocation = window.location
|
|
127
|
+
|
|
128
|
+
const mockLocation = {
|
|
129
|
+
replace: vi.fn(),
|
|
130
|
+
href: 'http://localhost:3000',
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
Object.defineProperty(window, 'location', {
|
|
134
|
+
value: mockLocation,
|
|
135
|
+
writable: true,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
render(
|
|
139
|
+
<SanityApp sanityConfigs={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
140
|
+
<div>Test Child</div>
|
|
141
|
+
</SanityApp>,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
// Wait for 1 second
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 1010))
|
|
146
|
+
|
|
147
|
+
// Add assertions based on your iframe-specific behavior
|
|
148
|
+
expect(mockLocation.replace).not.toHaveBeenCalled()
|
|
149
|
+
|
|
150
|
+
// Clean up the mock
|
|
151
|
+
Object.defineProperty(window, 'location', {
|
|
152
|
+
value: originalLocation,
|
|
153
|
+
writable: true,
|
|
154
|
+
})
|
|
155
|
+
})
|
|
54
156
|
})
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {type ReactElement} from 'react'
|
|
1
|
+
import {type SanityConfig} from '@sanity/sdk'
|
|
2
|
+
import {type ReactElement, useEffect} from 'react'
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import {SDKProvider} from './SDKProvider'
|
|
5
|
+
import {isInIframe, isLocalUrl} from './utils'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @public
|
|
9
9
|
*/
|
|
10
10
|
export interface SanityAppProps {
|
|
11
|
-
|
|
11
|
+
sanityConfigs: SanityConfig[]
|
|
12
12
|
children: React.ReactNode
|
|
13
|
+
fallback: React.ReactNode
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
const CORE_URL = 'https://core.sanity.io'
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
19
|
* @public
|
|
17
20
|
*
|
|
@@ -23,31 +26,65 @@ export interface SanityAppProps {
|
|
|
23
26
|
* @returns Your Sanity application, integrated with your Sanity configuration and application context
|
|
24
27
|
*
|
|
25
28
|
* @example
|
|
26
|
-
* ```
|
|
27
|
-
* import { SanityApp } from '@sanity/sdk-react
|
|
29
|
+
* ```tsx
|
|
30
|
+
* import { SanityApp } from '@sanity/sdk-react'
|
|
28
31
|
*
|
|
29
32
|
* import MyAppRoot from './Root'
|
|
30
33
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
34
|
+
* // Single project configuration
|
|
35
|
+
* const mySanityConfigs = [
|
|
36
|
+
* {
|
|
37
|
+
* projectId: 'my-project-id',
|
|
38
|
+
* dataset: 'production',
|
|
39
|
+
* },
|
|
40
|
+
* ]
|
|
41
|
+
*
|
|
42
|
+
* // Or multiple project configurations
|
|
43
|
+
* const multipleConfigs = [
|
|
44
|
+
* // Configuration for your main project. This will be used as the default project for all hooks if no resource ID override is provided.
|
|
45
|
+
* {
|
|
46
|
+
* projectId: 'marketing-website-project',
|
|
47
|
+
* dataset: 'production',
|
|
48
|
+
* },
|
|
49
|
+
* // Configuration for a separate blog project
|
|
50
|
+
* {
|
|
51
|
+
* projectId: 'blog-project',
|
|
52
|
+
* dataset: 'production',
|
|
53
|
+
* },
|
|
54
|
+
* // Configuration for a separate ecommerce project
|
|
55
|
+
* {
|
|
56
|
+
* projectId: 'ecommerce-project',
|
|
57
|
+
* dataset: 'production',
|
|
58
|
+
* }
|
|
59
|
+
* ]
|
|
35
60
|
*
|
|
36
61
|
* export default function MyApp() {
|
|
37
62
|
* return (
|
|
38
|
-
* <SanityApp
|
|
63
|
+
* <SanityApp sanityConfigs={mySanityConfigs}>
|
|
39
64
|
* <MyAppRoot />
|
|
40
65
|
* </SanityApp>
|
|
41
66
|
* )
|
|
42
67
|
* }
|
|
43
68
|
* ```
|
|
44
69
|
*/
|
|
45
|
-
export function SanityApp({
|
|
46
|
-
|
|
70
|
+
export function SanityApp({sanityConfigs, children, fallback}: SanityAppProps): ReactElement {
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
let timeout: NodeJS.Timeout | undefined
|
|
73
|
+
|
|
74
|
+
if (!isInIframe() && !isLocalUrl(window)) {
|
|
75
|
+
// If the app is not running in an iframe and is not a local url, redirect to core.
|
|
76
|
+
timeout = setTimeout(() => {
|
|
77
|
+
// eslint-disable-next-line no-console
|
|
78
|
+
console.warn('Redirecting to core', CORE_URL)
|
|
79
|
+
window.location.replace(CORE_URL)
|
|
80
|
+
}, 1000)
|
|
81
|
+
}
|
|
82
|
+
return () => clearTimeout(timeout)
|
|
83
|
+
}, [sanityConfigs])
|
|
47
84
|
|
|
48
85
|
return (
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
</
|
|
86
|
+
<SDKProvider sanityConfigs={sanityConfigs} fallback={fallback}>
|
|
87
|
+
{children}
|
|
88
|
+
</SDKProvider>
|
|
52
89
|
)
|
|
53
90
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {AuthStateType} from '@sanity/sdk'
|
|
2
|
-
import {useAuthState} from '@sanity/sdk-react
|
|
2
|
+
import {useAuthState} from '@sanity/sdk-react'
|
|
3
3
|
import {screen, waitFor} from '@testing-library/react'
|
|
4
4
|
import {beforeEach, describe, expect, it, type MockInstance, vi} from 'vitest'
|
|
5
5
|
|
|
@@ -13,8 +13,8 @@ vi.mock('../../hooks/auth/useAuthState', () => ({
|
|
|
13
13
|
vi.mock('../../hooks/auth/useLoginUrls', () => ({
|
|
14
14
|
useLoginUrls: vi.fn(() => [{title: 'Provider A', url: 'https://provider-a.com/auth'}]),
|
|
15
15
|
}))
|
|
16
|
-
vi.mock('../../hooks/auth/
|
|
17
|
-
|
|
16
|
+
vi.mock('../../hooks/auth/useHandleAuthCallback', () => ({
|
|
17
|
+
useHandleAuthCallback: vi.fn(() => async () => {}),
|
|
18
18
|
}))
|
|
19
19
|
vi.mock('../../hooks/auth/useLogOut', () => ({
|
|
20
20
|
useLogOut: vi.fn(() => async () => {}),
|
|
@@ -54,7 +54,7 @@ describe('AuthBoundary', () => {
|
|
|
54
54
|
renderWithWrappers(<AuthBoundary>Protected Content</AuthBoundary>)
|
|
55
55
|
|
|
56
56
|
// The login screen should show "Choose login provider" by default
|
|
57
|
-
expect(screen.getByText('Choose login provider
|
|
57
|
+
expect(screen.getByText('Choose login provider')).toBeInTheDocument()
|
|
58
58
|
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument()
|
|
59
59
|
})
|
|
60
60
|
|
|
@@ -3,6 +3,7 @@ import {useMemo} from 'react'
|
|
|
3
3
|
import {ErrorBoundary, type FallbackProps} from 'react-error-boundary'
|
|
4
4
|
|
|
5
5
|
import {useAuthState} from '../../hooks/auth/useAuthState'
|
|
6
|
+
import {isInIframe} from '../utils'
|
|
6
7
|
import {AuthError} from './AuthError'
|
|
7
8
|
import {Login} from './Login'
|
|
8
9
|
import {LoginCallback} from './LoginCallback'
|
|
@@ -10,9 +11,18 @@ import {LoginError, type LoginErrorProps} from './LoginError'
|
|
|
10
11
|
import {type LoginLayoutProps} from './LoginLayout'
|
|
11
12
|
|
|
12
13
|
// Only import bridge if we're in an iframe. This assumes that the app is
|
|
13
|
-
// running
|
|
14
|
-
if (
|
|
15
|
-
|
|
14
|
+
// running within SanityOS if it is in an iframe.
|
|
15
|
+
if (isInIframe()) {
|
|
16
|
+
const parsedUrl = new URL(window.location.href)
|
|
17
|
+
const mode = new URLSearchParams(parsedUrl.hash.slice(1)).get('mode')
|
|
18
|
+
const script = document.createElement('script')
|
|
19
|
+
script.src =
|
|
20
|
+
mode === 'core-ui--staging'
|
|
21
|
+
? 'https://core.sanity-cdn.work/bridge.js'
|
|
22
|
+
: 'https://core.sanity-cdn.com/bridge.js'
|
|
23
|
+
script.type = 'module'
|
|
24
|
+
script.async = true
|
|
25
|
+
document.head.appendChild(script)
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
/**
|
|
@@ -14,7 +14,7 @@ vi.mock('../../hooks/auth/useLoginUrls', () => ({
|
|
|
14
14
|
describe('Login', () => {
|
|
15
15
|
it('renders login providers', () => {
|
|
16
16
|
renderWithWrappers(<Login />)
|
|
17
|
-
expect(screen.getByText('Choose login provider
|
|
17
|
+
expect(screen.getByText('Choose login provider')).toBeInTheDocument()
|
|
18
18
|
expect(screen.getByRole('link', {name: 'Provider A'})).toHaveAttribute(
|
|
19
19
|
'href',
|
|
20
20
|
'https://provider-a.com/auth',
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {Box, Button, Flex, Heading, Spinner, Stack} from '@sanity/ui'
|
|
2
1
|
import {type JSX, Suspense} from 'react'
|
|
3
2
|
|
|
4
3
|
import {useLoginUrls} from '../../hooks/auth/useLoginUrls'
|
|
@@ -14,21 +13,13 @@ import {LoginLayout, type LoginLayoutProps} from './LoginLayout'
|
|
|
14
13
|
export function Login({header, footer}: LoginLayoutProps): JSX.Element {
|
|
15
14
|
return (
|
|
16
15
|
<LoginLayout header={header} footer={footer}>
|
|
17
|
-
<
|
|
18
|
-
Choose login provider
|
|
19
|
-
</Heading>
|
|
16
|
+
<div className="sc-login">
|
|
17
|
+
<h1 className="sc-login__title">Choose login provider</h1>
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<Spinner />
|
|
26
|
-
</Flex>
|
|
27
|
-
</Box>
|
|
28
|
-
}
|
|
29
|
-
>
|
|
30
|
-
<Providers />
|
|
31
|
-
</Suspense>
|
|
19
|
+
<Suspense fallback={<div className="sc-login__loading">Loading…</div>}>
|
|
20
|
+
<Providers />
|
|
21
|
+
</Suspense>
|
|
22
|
+
</div>
|
|
32
23
|
</LoginLayout>
|
|
33
24
|
)
|
|
34
25
|
}
|
|
@@ -37,18 +28,12 @@ function Providers() {
|
|
|
37
28
|
const loginUrls = useLoginUrls()
|
|
38
29
|
|
|
39
30
|
return (
|
|
40
|
-
<
|
|
31
|
+
<div className="sc-login-providers">
|
|
41
32
|
{loginUrls.map(({title, url}) => (
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
href={url}
|
|
46
|
-
mode="ghost"
|
|
47
|
-
text={title}
|
|
48
|
-
textAlign="center"
|
|
49
|
-
fontSize={2}
|
|
50
|
-
></Button>
|
|
33
|
+
<a key={url} href={url}>
|
|
34
|
+
{title}
|
|
35
|
+
</a>
|
|
51
36
|
))}
|
|
52
|
-
</
|
|
37
|
+
</div>
|
|
53
38
|
)
|
|
54
39
|
}
|
|
@@ -3,9 +3,9 @@ import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
|
3
3
|
|
|
4
4
|
import {renderWithWrappers} from './authTestHelpers'
|
|
5
5
|
|
|
6
|
-
// Mock `
|
|
7
|
-
vi.mock('../../hooks/auth/
|
|
8
|
-
|
|
6
|
+
// Mock `useHandleAuthCallback`
|
|
7
|
+
vi.mock('../../hooks/auth/useHandleAuthCallback', () => ({
|
|
8
|
+
useHandleAuthCallback: vi.fn(() => async (url: string) => {
|
|
9
9
|
const parsedUrl = new URL(url)
|
|
10
10
|
const sid = new URLSearchParams(parsedUrl.hash.slice(1)).get('sid')
|
|
11
11
|
if (sid === 'valid') {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {Flex, Heading, Spinner} from '@sanity/ui'
|
|
2
1
|
import {useEffect} from 'react'
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import {useHandleAuthCallback} from '../../hooks/auth/useHandleAuthCallback'
|
|
5
4
|
import {LoginLayout, type LoginLayoutProps} from './LoginLayout'
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -13,27 +12,25 @@ import {LoginLayout, type LoginLayoutProps} from './LoginLayout'
|
|
|
13
12
|
* @alpha
|
|
14
13
|
*/
|
|
15
14
|
export function LoginCallback({header, footer}: LoginLayoutProps): React.ReactNode {
|
|
16
|
-
const
|
|
15
|
+
const handleAuthCallback = useHandleAuthCallback()
|
|
17
16
|
|
|
18
17
|
useEffect(() => {
|
|
19
18
|
const url = new URL(location.href)
|
|
20
|
-
|
|
19
|
+
handleAuthCallback(url.toString()).then((replacementLocation) => {
|
|
21
20
|
if (replacementLocation) {
|
|
22
21
|
// history API with `replaceState` is used to prevent a reload but still
|
|
23
22
|
// remove the short-lived token from the URL
|
|
24
23
|
history.replaceState(null, '', replacementLocation)
|
|
25
24
|
}
|
|
26
25
|
})
|
|
27
|
-
}, [
|
|
26
|
+
}, [handleAuthCallback])
|
|
28
27
|
|
|
29
28
|
return (
|
|
30
29
|
<LoginLayout header={header} footer={footer}>
|
|
31
|
-
<
|
|
32
|
-
Logging you in
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<Spinner />
|
|
36
|
-
</Flex>
|
|
30
|
+
<div className="sc-login-callback">
|
|
31
|
+
<h1 className="sc-login-callback__title">Logging you in…</h1>
|
|
32
|
+
<div className="sc-login-callback__loading">Loading…</div>
|
|
33
|
+
</div>
|
|
37
34
|
</LoginLayout>
|
|
38
35
|
)
|
|
39
36
|
}
|