@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.
Files changed (100) hide show
  1. package/README.md +33 -126
  2. package/dist/index.d.ts +4811 -2
  3. package/dist/index.js +1069 -2
  4. package/dist/index.js.map +1 -1
  5. package/package.json +23 -45
  6. package/src/_exports/index.ts +66 -10
  7. package/src/components/Login/LoginLinks.test.tsx +90 -0
  8. package/src/components/Login/LoginLinks.tsx +58 -0
  9. package/src/components/SDKProvider.test.tsx +79 -0
  10. package/src/components/SDKProvider.tsx +42 -0
  11. package/src/components/SanityApp.test.tsx +104 -2
  12. package/src/components/SanityApp.tsx +54 -17
  13. package/src/components/auth/AuthBoundary.test.tsx +4 -4
  14. package/src/components/auth/AuthBoundary.tsx +13 -3
  15. package/src/components/auth/Login.test.tsx +1 -1
  16. package/src/components/auth/Login.tsx +11 -26
  17. package/src/components/auth/LoginCallback.test.tsx +3 -3
  18. package/src/components/auth/LoginCallback.tsx +8 -11
  19. package/src/components/auth/LoginError.tsx +12 -8
  20. package/src/components/auth/LoginFooter.tsx +13 -20
  21. package/src/components/auth/LoginLayout.tsx +8 -9
  22. package/src/components/auth/authTestHelpers.tsx +1 -8
  23. package/src/components/utils.ts +22 -0
  24. package/src/context/SanityInstanceContext.ts +4 -0
  25. package/src/context/SanityProvider.test.tsx +1 -1
  26. package/src/context/SanityProvider.tsx +10 -8
  27. package/src/hooks/_synchronous-groq-js.mjs +4 -0
  28. package/src/hooks/auth/useAuthState.tsx +0 -2
  29. package/src/hooks/auth/useCurrentUser.tsx +27 -20
  30. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
  31. package/src/hooks/auth/useDashboardOrganizationId.tsx +29 -0
  32. package/src/hooks/auth/useHandleAuthCallback.test.tsx +16 -0
  33. package/src/hooks/auth/{useHandleCallback.tsx → useHandleAuthCallback.tsx} +6 -6
  34. package/src/hooks/auth/useLogOut.test.tsx +2 -2
  35. package/src/hooks/client/useClient.ts +9 -30
  36. package/src/hooks/comlink/useFrameConnection.test.tsx +55 -10
  37. package/src/hooks/comlink/useFrameConnection.ts +39 -43
  38. package/src/hooks/comlink/useManageFavorite.test.ts +111 -0
  39. package/src/hooks/comlink/useManageFavorite.ts +130 -0
  40. package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +81 -0
  41. package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +106 -0
  42. package/src/hooks/comlink/useWindowConnection.test.ts +53 -12
  43. package/src/hooks/comlink/useWindowConnection.ts +69 -29
  44. package/src/hooks/context/useSanityInstance.test.tsx +1 -1
  45. package/src/hooks/context/useSanityInstance.ts +21 -5
  46. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +178 -0
  47. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +123 -0
  48. package/src/hooks/dashboard/useStudioWorkspacesByResourceId.test.tsx +278 -0
  49. package/src/hooks/dashboard/useStudioWorkspacesByResourceId.ts +92 -0
  50. package/src/hooks/datasets/useDatasets.ts +40 -0
  51. package/src/hooks/document/useApplyDocumentActions.test.ts +25 -0
  52. package/src/hooks/document/useApplyDocumentActions.ts +75 -0
  53. package/src/hooks/document/useDocument.test.ts +81 -0
  54. package/src/hooks/document/useDocument.ts +107 -0
  55. package/src/hooks/document/useDocumentEvent.test.ts +63 -0
  56. package/src/hooks/document/useDocumentEvent.ts +54 -0
  57. package/src/hooks/document/useDocumentPermissions.ts +84 -0
  58. package/src/hooks/document/useDocumentSyncStatus.test.ts +16 -0
  59. package/src/hooks/document/useDocumentSyncStatus.ts +33 -0
  60. package/src/hooks/document/useEditDocument.test.ts +179 -0
  61. package/src/hooks/document/useEditDocument.ts +195 -0
  62. package/src/hooks/documents/useDocuments.test.tsx +152 -0
  63. package/src/hooks/documents/useDocuments.ts +174 -0
  64. package/src/hooks/helpers/createCallbackHook.tsx +3 -2
  65. package/src/hooks/helpers/createStateSourceHook.test.tsx +66 -0
  66. package/src/hooks/helpers/createStateSourceHook.tsx +29 -10
  67. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +259 -0
  68. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +290 -0
  69. package/src/hooks/preview/usePreview.test.tsx +6 -6
  70. package/src/hooks/preview/usePreview.tsx +12 -9
  71. package/src/hooks/projection/useProjection.test.tsx +218 -0
  72. package/src/hooks/projection/useProjection.ts +147 -0
  73. package/src/hooks/projects/useProject.ts +48 -0
  74. package/src/hooks/projects/useProjects.ts +45 -0
  75. package/src/hooks/query/useQuery.test.tsx +188 -0
  76. package/src/hooks/query/useQuery.ts +103 -0
  77. package/src/hooks/users/useUsers.test.ts +163 -0
  78. package/src/hooks/users/useUsers.ts +107 -0
  79. package/src/utils/getEnv.ts +21 -0
  80. package/src/version.ts +8 -0
  81. package/dist/_chunks-es/context.js +0 -8
  82. package/dist/_chunks-es/context.js.map +0 -1
  83. package/dist/_chunks-es/useLogOut.js +0 -44
  84. package/dist/_chunks-es/useLogOut.js.map +0 -1
  85. package/dist/components.d.ts +0 -111
  86. package/dist/components.js +0 -153
  87. package/dist/components.js.map +0 -1
  88. package/dist/context.d.ts +0 -45
  89. package/dist/context.js +0 -5
  90. package/dist/context.js.map +0 -1
  91. package/dist/hooks.d.ts +0 -3485
  92. package/dist/hooks.js +0 -167
  93. package/dist/hooks.js.map +0 -1
  94. package/src/_exports/components.ts +0 -2
  95. package/src/_exports/context.ts +0 -2
  96. package/src/_exports/hooks.ts +0 -27
  97. package/src/hooks/auth/useHandleCallback.test.tsx +0 -16
  98. package/src/hooks/client/useClient.test.tsx +0 -130
  99. package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
  100. 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 sanityConfig={mockSanityConfig}>
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 {createSanityInstance, type SanityConfig} from '@sanity/sdk'
2
- import {type ReactElement} from 'react'
1
+ import {type SanityConfig} from '@sanity/sdk'
2
+ import {type ReactElement, useEffect} from 'react'
3
3
 
4
- import {SanityProvider} from '../context/SanityProvider'
5
- import {AuthBoundary} from './auth/AuthBoundary'
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
- sanityConfig: SanityConfig
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
- * const mySanityConfig = {
32
- * procectId: 'my-project-id',
33
- * dataset: 'production',
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 sanityConfig={mySanityConfig}>
63
+ * <SanityApp sanityConfigs={mySanityConfigs}>
39
64
  * <MyAppRoot />
40
65
  * </SanityApp>
41
66
  * )
42
67
  * }
43
68
  * ```
44
69
  */
45
- export function SanityApp({sanityConfig, children}: SanityAppProps): ReactElement {
46
- const sanityInstance = createSanityInstance(sanityConfig)
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
- <SanityProvider sanityInstance={sanityInstance}>
50
- <AuthBoundary>{children}</AuthBoundary>
51
- </SanityProvider>
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/hooks'
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/useHandleCallback', () => ({
17
- useHandleCallback: vi.fn(() => async () => {}),
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:')).toBeInTheDocument()
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 withing SanityOS if it is in an iframe.
14
- if (typeof window !== 'undefined' && window.self !== window.top) {
15
- import('@sanity/os/bridge')
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:')).toBeInTheDocument()
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
- <Heading as="h6" align="center">
18
- Choose login provider:
19
- </Heading>
16
+ <div className="sc-login">
17
+ <h1 className="sc-login__title">Choose login provider</h1>
20
18
 
21
- <Suspense
22
- fallback={
23
- <Box padding={5}>
24
- <Flex align="center" justify="center">
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
- <Stack space={3} marginY={5}>
31
+ <div className="sc-login-providers">
41
32
  {loginUrls.map(({title, url}) => (
42
- <Button
43
- key={url}
44
- as="a"
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
- </Stack>
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 `useHandleCallback`
7
- vi.mock('../../hooks/auth/useHandleCallback', () => ({
8
- useHandleCallback: vi.fn(() => async (url: string) => {
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 {useHandleCallback} from '../../hooks/auth/useHandleCallback'
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 handleCallback = useHandleCallback()
15
+ const handleAuthCallback = useHandleAuthCallback()
17
16
 
18
17
  useEffect(() => {
19
18
  const url = new URL(location.href)
20
- handleCallback(url.toString()).then((replacementLocation) => {
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
- }, [handleCallback])
26
+ }, [handleAuthCallback])
28
27
 
29
28
  return (
30
29
  <LoginLayout header={header} footer={footer}>
31
- <Heading as="h6" align="center">
32
- Logging you in
33
- </Heading>
34
- <Flex paddingY={5} align="center" justify="center">
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
  }