@sanity/sdk-react 3.3.0-rc.0 → 3.3.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 (42) hide show
  1. package/dist/_exports/dashboard-internal.d.ts +2 -2
  2. package/dist/_exports/dashboard.d.ts +57 -1
  3. package/dist/_exports/dashboard.d.ts.map +1 -1
  4. package/dist/_exports/dashboard.js +69 -38
  5. package/dist/_exports/dashboard.js.map +1 -1
  6. package/dist/index.d.ts +5 -139
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +27 -145
  9. package/dist/index.js.map +1 -1
  10. package/dist/{useStudioWorkspacesByProjectIdDataset-BZ_Ud887.js → useStudioWorkspacesByProjectIdDataset-B5A5kBH9.js} +85 -5
  11. package/dist/useStudioWorkspacesByProjectIdDataset-B5A5kBH9.js.map +1 -0
  12. package/package.json +8 -8
  13. package/src/_exports/dashboard-internal.ts +6 -1
  14. package/src/_exports/dashboard.test-d.ts +30 -4
  15. package/src/_exports/dashboard.ts +3 -0
  16. package/src/_exports/sdk-react.ts +0 -3
  17. package/src/components/auth/AuthBoundary.test.tsx +2 -81
  18. package/src/components/auth/AuthBoundary.tsx +3 -17
  19. package/src/components/auth/LoginCallback.test.tsx +7 -46
  20. package/src/components/auth/LoginCallback.tsx +4 -22
  21. package/src/hooks/dashboard/useApplicationBasePath.test.tsx +77 -0
  22. package/src/hooks/dashboard/useApplicationBasePath.ts +23 -0
  23. package/src/hooks/dashboard/useApplications.test.tsx +12 -3
  24. package/src/hooks/dashboard/useApplications.ts +5 -24
  25. package/src/hooks/dashboard/useAuthToken.test.tsx +48 -0
  26. package/src/hooks/dashboard/useAuthToken.ts +20 -0
  27. package/src/hooks/dashboard/useCurrentUser.test.tsx +57 -0
  28. package/src/hooks/dashboard/useCurrentUser.ts +22 -0
  29. package/src/hooks/dashboard/useOrganizationId.test.tsx +52 -1
  30. package/src/hooks/dashboard/useOrganizationId.tsx +15 -2
  31. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +142 -1
  32. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +58 -1
  33. package/src/hooks/dashboard/useWindowTitle.test.ts +23 -0
  34. package/src/hooks/dashboard/useWindowTitle.ts +14 -0
  35. package/src/hooks/installations/useInstallations.test-d.ts +10 -0
  36. package/dist/useStudioWorkspacesByProjectIdDataset-BZ_Ud887.js.map +0 -1
  37. package/src/hooks/auth/useHandleOAuthCallback.test.tsx +0 -16
  38. package/src/hooks/auth/useHandleOAuthCallback.tsx +0 -49
  39. package/src/hooks/auth/useOAuthAuthorize.test.tsx +0 -16
  40. package/src/hooks/auth/useOAuthAuthorize.tsx +0 -28
  41. package/src/hooks/auth/useOAuthTokens.test.tsx +0 -240
  42. package/src/hooks/auth/useOAuthTokens.tsx +0 -95
@@ -1,7 +1,7 @@
1
1
  import { c } from "react-compiler-runtime";
2
2
  import "@sanity/client";
3
3
  import { AuthStateType, getAuthState, getDashboardOrganizationId, setAuthToken } from "@sanity/sdk";
4
- import { getDashboardMessageBus } from "@sanity/sdk/_internal";
4
+ import { getApplicationOrigin, getDashboardMessageBus, getTopicState, isDashboardEnvironment, resolveTopic } from "@sanity/sdk/_internal";
5
5
  import * as React$1 from "react";
6
6
  import { createContext, useContext, useEffect, useRef, useState, useSyncExternalStore } from "react";
7
7
  import { SDK_CHANNEL_NAME, SDK_NODE_NAME } from "@sanity/message-protocol";
@@ -214,6 +214,9 @@ const DashboardTokenRefreshProvider = (t0) => {
214
214
  *
215
215
  * A React hook that retrieves the dashboard organization ID that is currently selected in the Sanity Dashboard.
216
216
  *
217
+ * Works in both Dashboard runtimes: it reads the `organizations.current` message bus topic when a
218
+ * host has installed the bus, and falls back to the Comlink connection otherwise.
219
+ *
217
220
  * @example
218
221
  * ```tsx
219
222
  * function DashboardComponent() {
@@ -229,13 +232,53 @@ const DashboardTokenRefreshProvider = (t0) => {
229
232
  * @returns The dashboard organization ID (string | undefined)
230
233
  */
231
234
  function useOrganizationId() {
232
- let $ = c(2), instance = useSanityInstance(), t0;
233
- $[0] === instance ? t0 = $[1] : (t0 = getDashboardOrganizationId(instance), $[0] = instance, $[1] = t0);
235
+ let $ = c(9), instance = useSanityInstance(), t0;
236
+ bb0: {
237
+ if (!isDashboardEnvironment()) {
238
+ let t1;
239
+ $[0] === instance ? t1 = $[1] : (t1 = getDashboardOrganizationId(instance), $[0] = instance, $[1] = t1), t0 = t1;
240
+ break bb0;
241
+ }
242
+ let t1;
243
+ $[2] === instance ? t1 = $[3] : (t1 = getTopicState(instance, "organizations.current"), $[2] = instance, $[3] = t1);
244
+ let source = t1, t2;
245
+ $[4] === source ? t2 = $[5] : (t2 = () => source.getCurrent()?.id ?? void 0, $[4] = source, $[5] = t2);
246
+ let t3;
247
+ $[6] !== source.subscribe || $[7] !== t2 ? (t3 = {
248
+ subscribe: source.subscribe,
249
+ getCurrent: t2
250
+ }, $[6] = source.subscribe, $[7] = t2, $[8] = t3) : t3 = $[8], t0 = t3;
251
+ }
234
252
  let { subscribe, getCurrent } = t0;
235
253
  return useSyncExternalStore(subscribe, getCurrent);
236
254
  }
237
255
  /**
256
+ * Returns the current value of a dashboard state topic and follows later updates.
257
+ *
258
+ * The hook suspends until the topic publishes its first value, using the message bus query
259
+ * deadline. A topic declared with `TopicResult` resolves to its successful value; a
260
+ * failed result throws a `TopicError` to the nearest error boundary.
261
+ *
262
+ * @example
263
+ * ```tsx
264
+ * function ForegroundApplication() {
265
+ * const foregroundId = useTopic('applications.foreground')
266
+ * return <span>{foregroundId ?? 'No application in the foreground'}</span>
267
+ * }
268
+ * ```
269
+ *
270
+ * @public
271
+ */
272
+ const useTopic = createStateSourceHook({
273
+ getState: getTopicState,
274
+ shouldSuspend: (instance, topic) => getTopicState(instance, topic).getCurrent() === void 0,
275
+ suspender: resolveTopic
276
+ });
277
+ /**
238
278
  * Hook that fetches studio workspaces and organizes them by projectId:dataset
279
+ *
280
+ * Works in both Dashboard runtimes: it derives workspaces from the `applications.list` message
281
+ * bus topic when a host has installed the bus, and falls back to the Comlink connection otherwise.
239
282
  * @internal
240
283
  *
241
284
  * @example
@@ -269,6 +312,43 @@ function useOrganizationId() {
269
312
  * ```
270
313
  */
271
314
  function useStudioWorkspacesByProjectIdDataset() {
315
+ return isDashboardEnvironment() ? useBusStudioWorkspaces() : useComlinkStudioWorkspaces();
316
+ }
317
+ function toResources(application) {
318
+ if (application.type !== "studio") return [];
319
+ let url = getApplicationOrigin(application) ?? "";
320
+ return (application.activeDeployment?.workspaces ?? []).map((workspace) => ({
321
+ id: workspace.id,
322
+ name: workspace.name,
323
+ title: workspace.title ?? application.title,
324
+ basePath: workspace.basePath ?? "",
325
+ projectId: workspace.projectId,
326
+ dataset: workspace.dataset,
327
+ type: "studio",
328
+ userApplicationId: application.id,
329
+ url
330
+ }));
331
+ }
332
+ function toWorkspaceMap(applications) {
333
+ let workspaceMap = {};
334
+ for (let resource of applications.flatMap(toResources)) {
335
+ let key = `${resource.projectId}:${resource.dataset}`;
336
+ workspaceMap[key] ??= [], workspaceMap[key].push(resource);
337
+ }
338
+ return workspaceMap;
339
+ }
340
+ function useBusStudioWorkspaces() {
341
+ let $ = c(6), applications = useTopic("applications.list"), t0;
342
+ $[0] === applications ? t0 = $[1] : (t0 = applications ?? [], $[0] = applications, $[1] = t0);
343
+ let t1;
344
+ $[2] === t0 ? t1 = $[3] : (t1 = toWorkspaceMap(t0), $[2] = t0, $[3] = t1);
345
+ let workspacesByProjectIdAndDataset = t1, t2;
346
+ return $[4] === workspacesByProjectIdAndDataset ? t2 = $[5] : (t2 = {
347
+ workspacesByProjectIdAndDataset,
348
+ error: null
349
+ }, $[4] = workspacesByProjectIdAndDataset, $[5] = t2), t2;
350
+ }
351
+ function useComlinkStudioWorkspaces() {
272
352
  let $ = c(8), t0;
273
353
  $[0] === Symbol.for("react.memo_cache_sentinel") ? (t0 = {}, $[0] = t0) : t0 = $[0];
274
354
  let [workspacesByProjectIdAndDataset, setWorkspacesByProjectIdAndDataset] = useState(t0), [error, setError] = useState(null), t1;
@@ -309,6 +389,6 @@ function useStudioWorkspacesByProjectIdDataset() {
309
389
  error
310
390
  }, $[5] = error, $[6] = workspacesByProjectIdAndDataset, $[7] = t4) : t4 = $[7], t4;
311
391
  }
312
- export { useAuthState as a, SanityInstanceContext as c, useWindowConnection as i, useOrganizationId as n, createStateSourceHook as o, DashboardTokenRefreshProvider as r, useSanityInstance as s, useStudioWorkspacesByProjectIdDataset as t };
392
+ export { useWindowConnection as a, useSanityInstance as c, DashboardTokenRefreshProvider as i, SanityInstanceContext as l, useTopic as n, useAuthState as o, useOrganizationId as r, createStateSourceHook as s, useStudioWorkspacesByProjectIdDataset as t };
313
393
 
314
- //# sourceMappingURL=useStudioWorkspacesByProjectIdDataset-BZ_Ud887.js.map
394
+ //# sourceMappingURL=useStudioWorkspacesByProjectIdDataset-B5A5kBH9.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStudioWorkspacesByProjectIdDataset-B5A5kBH9.js","names":["SanityInstance","createContext","SanityInstanceContext","SanityInstance","useContext","SanityInstanceContext","useSanityInstance","instance","Error","SanityConfig","SanityInstance","StateSource","useSyncExternalStore","useSanityInstance","StateSourceFactory","instance","params","TParams","TState","CreateStateSourceHookOptions","getState","shouldSuspend","suspender","Promise","getConfig","createStateSourceHook","options","suspense","undefined","useHook","t0","$","_c","t1","state","subscribe","getCurrent","AuthState","getAuthState","createStateSourceHook","useAuthState","MessageData","NodeInput","SanityInstance","StateSource","FrameMessage","getNodeState","NodeState","WindowMessage","useCallback","useEffect","useRef","filter","firstValueFrom","useSanityInstance","createStateSourceHook","WindowMessageHandler","event","TFrameMessage","UseWindowConnectionOptions","name","connectTo","onMessage","Record","TMessage","WindowConnection","sendMessage","type","TType","data","Extract","fetch","options","signal","AbortSignal","suppressWarnings","responseTimeout","Promise","TResponse","useNodeState","getState","instance","nodeInput","shouldSuspend","getCurrent","undefined","suspender","observable","pipe","Boolean","useWindowConnection","t0","$","_c","t1","node","t2","Symbol","for","messageUnsubscribers","t3","Object","entries","forEach","t4","handler","messageUnsubscribe","on","current","push","_temp","t5","type_0","post","t6","type_1","data_0","fetchOptions","t7","unsubscribe","React","MODULE_SLOT_KEY","Symbol","for","ModuleContext","Context","ModuleSlot","WeakMap","createContext","getDashboardModuleContext","globals","globalThis","slot","key","context","get","undefined","set","ClientError","AuthStateType","setAuthToken","getDashboardMessageBus","MessageBus","React","PropsWithChildren","useContext","useEffect","useRef","useState","defer","of","catchError","getDashboardModuleContext","useAuthState","useSanityInstance","DashboardTokenRefresh","t0","$","_c","children","messageBus","instance","authState","processed401ErrorRef","t1","t2","subscription","subscribe","pipe","_temp","token","unsubscribe","t3","error","type","has401Error","ERROR","statusCode","current","emit","undefined","catch","_temp2","t4","console","warn","DashboardTokenRefreshProvider","FC","Symbol","for","moduleId","getDashboardOrganizationId","OrganizationBase","getTopicState","isDashboardEnvironment","useMemo","useSyncExternalStore","useSanityInstance","CurrentOrganization","Pick","useOrganizationId","$","_c","instance","t0","bb0","t1","source","t2","getCurrent","id","undefined","t3","subscribe","getTopicState","resolveTopic","StateTopic","TopicData","createStateSourceHook","useTopic","getState","shouldSuspend","instance","topic","getCurrent","undefined","suspender","K","SDK_CHANNEL_NAME","SDK_NODE_NAME","getApplicationOrigin","isDashboardEnvironment","TopicData","useEffect","useMemo","useState","useWindowConnection","useTopic","DashboardResource","id","name","title","basePath","projectId","dataset","type","userApplicationId","url","WorkspacesByProjectIdDataset","key","StudioWorkspacesResult","workspacesByProjectIdAndDataset","error","DashboardApplications","NonNullable","useStudioWorkspacesByProjectIdDataset","useBusStudioWorkspaces","useComlinkStudioWorkspaces","toResources","application","activeDeployment","workspaces","map","workspace","toWorkspaceMap","applications","workspaceMap","resource","flatMap","const","push","$","_c","t0","t1","t2","Symbol","for","setWorkspacesByProjectIdAndDataset","setError","connectTo","fetch","t3","fetchWorkspaces","signal","data","undefined","noProjectIdAndDataset","context","availableResources","forEach","length","t4","err","Error","controller","AbortController","abort"],"sources":["../src/context/SanityInstanceContext.ts","../src/hooks/context/useSanityInstance.ts","../src/hooks/helpers/createStateSourceHook.tsx","../src/hooks/auth/useAuthState.tsx","../src/hooks/comlink/useWindowConnection.ts","../src/dashboard/module.ts","../src/context/DashboardTokenRefresh.tsx","../src/hooks/dashboard/useOrganizationId.tsx","../src/hooks/dashboard/useTopic.ts","../src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts"],"sourcesContent":["import {type SanityInstance} from '@sanity/sdk'\nimport {createContext} from 'react'\n\nexport const SanityInstanceContext = createContext<SanityInstance | null>(null)\n","import {type SanityInstance} from '@sanity/sdk'\nimport {useContext} from 'react'\n\nimport {SanityInstanceContext} from '../../context/SanityInstanceContext'\n\n/**\n * Retrieves the current Sanity instance from context\n *\n * @public\n *\n * @category Platform\n * @returns The current Sanity instance\n *\n * @remarks\n * This hook accesses the nearest Sanity instance from the React context.\n * The hook must be used within a component wrapped by a `ResourceProvider` or `SanityApp`.\n *\n * @example Get the current instance\n * ```tsx\n * const instance = useSanityInstance()\n * console.log(instance.config.projectId)\n * ```\n *\n * @throws Error if no SanityInstance is found in context\n */\nexport const useSanityInstance = (): SanityInstance => {\n const instance = useContext(SanityInstanceContext)\n\n if (!instance) {\n throw new Error(\n `SanityInstance context not found. Please ensure that your component is wrapped in a ResourceProvider or a SanityApp component.`,\n )\n }\n\n return instance\n}\n","import {type SanityConfig, type SanityInstance, type StateSource} from '@sanity/sdk'\nimport {useSyncExternalStore} from 'react'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\n\ntype StateSourceFactory<TParams extends unknown[], TState> = (\n instance: SanityInstance,\n ...params: TParams\n) => StateSource<TState>\n\ninterface CreateStateSourceHookOptions<TParams extends unknown[], TState> {\n getState: StateSourceFactory<TParams, TState>\n shouldSuspend?: (instance: SanityInstance, ...params: TParams) => boolean\n suspender?: (instance: SanityInstance, ...params: TParams) => Promise<unknown>\n getConfig?: (...params: TParams) => SanityConfig | undefined\n}\n\nexport function createStateSourceHook<TParams extends unknown[], TState>(\n options: StateSourceFactory<TParams, TState> | CreateStateSourceHookOptions<TParams, TState>,\n): (...params: TParams) => TState {\n const getState = typeof options === 'function' ? options : options.getState\n const suspense = 'shouldSuspend' in options && 'suspender' in options ? options : undefined\n\n function useHook(...params: TParams) {\n const instance = useSanityInstance()\n\n if (suspense?.suspender && suspense?.shouldSuspend?.(instance, ...params)) {\n throw suspense.suspender(instance, ...params)\n }\n\n const state = getState(instance, ...params)\n return useSyncExternalStore(state.subscribe, state.getCurrent)\n }\n\n return useHook\n}\n","import {type AuthState, getAuthState} from '@sanity/sdk'\n\nimport {createStateSourceHook} from '../helpers/createStateSourceHook'\n\n/**\n * @internal\n * A React hook that subscribes to authentication state changes.\n *\n * This hook provides access to the current authentication state type from the Sanity auth store.\n * It automatically re-renders when the authentication state changes.\n *\n * @remarks\n * The hook uses `useSyncExternalStore` to safely subscribe to auth state changes\n * and ensure consistency between server and client rendering.\n *\n * @returns The current authentication state type\n *\n * @example\n * ```tsx\n * function AuthStatus() {\n * const authState = useAuthState()\n * return <div>Current auth state: {authState}</div>\n * }\n * ```\n */\nexport const useAuthState: () => AuthState = createStateSourceHook(getAuthState)\n","import {type MessageData, type NodeInput} from '@sanity/comlink'\nimport {type SanityInstance, type StateSource} from '@sanity/sdk'\nimport {\n type FrameMessage,\n getNodeState,\n type NodeState,\n type WindowMessage,\n} from '@sanity/sdk/comlink'\nimport {useCallback, useEffect, useRef} from 'react'\nimport {filter, firstValueFrom} from 'rxjs'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\nimport {createStateSourceHook} from '../helpers/createStateSourceHook'\n\n/**\n * @internal\n */\nexport type WindowMessageHandler<TFrameMessage extends FrameMessage> = (\n event: TFrameMessage['data'],\n) => TFrameMessage['response']\n\n/**\n * @internal\n */\nexport interface UseWindowConnectionOptions<TMessage extends FrameMessage> {\n name: string\n connectTo: string\n onMessage?: Record<TMessage['type'], WindowMessageHandler<TMessage>>\n}\n\n/**\n * @internal\n */\nexport interface WindowConnection<TMessage extends WindowMessage> {\n sendMessage: <TType extends TMessage['type']>(\n type: TType,\n data?: Extract<TMessage, {type: TType}>['data'],\n ) => void\n fetch: <TResponse>(\n type: string,\n data?: MessageData,\n options?: {\n signal?: AbortSignal\n suppressWarnings?: boolean\n responseTimeout?: number\n },\n ) => Promise<TResponse>\n}\n\nconst useNodeState = createStateSourceHook({\n getState: getNodeState as (\n instance: SanityInstance,\n nodeInput: NodeInput,\n ) => StateSource<NodeState>,\n shouldSuspend: (instance: SanityInstance, nodeInput: NodeInput) =>\n getNodeState(instance, nodeInput).getCurrent() === undefined,\n suspender: (instance: SanityInstance, nodeInput: NodeInput) => {\n return firstValueFrom(getNodeState(instance, nodeInput).observable.pipe(filter(Boolean)))\n },\n})\n\n/**\n * @internal\n * Hook to wrap a Comlink node in a React hook.\n * Our store functionality takes care of the lifecycle of the node,\n * as well as sharing a single node between invocations if they share the same name.\n *\n * Generally not to be used directly, but to be used as a dependency of\n * Comlink-powered hooks like `useStudioWorkspacesByProjectIdDataset`.\n */\nexport function useWindowConnection<\n TWindowMessage extends WindowMessage,\n TFrameMessage extends FrameMessage,\n>({\n name,\n connectTo,\n onMessage,\n}: UseWindowConnectionOptions<TFrameMessage>): WindowConnection<TWindowMessage> {\n const {node} = useNodeState({name, connectTo})\n const messageUnsubscribers = useRef<(() => void)[]>([])\n const instance = useSanityInstance()\n\n useEffect(() => {\n if (onMessage) {\n Object.entries(onMessage).forEach(([type, handler]) => {\n const messageUnsubscribe = node.on(type, handler as WindowMessageHandler<TFrameMessage>)\n if (messageUnsubscribe) {\n messageUnsubscribers.current.push(messageUnsubscribe)\n }\n })\n }\n\n return () => {\n messageUnsubscribers.current.forEach((unsubscribe) => unsubscribe())\n messageUnsubscribers.current = []\n }\n }, [instance, name, onMessage, node])\n\n const sendMessage = useCallback(\n (type: TWindowMessage['type'], data?: Extract<TWindowMessage, {type: typeof type}>['data']) => {\n node.post(type, data)\n },\n [node],\n )\n\n const fetch = useCallback(\n <TResponse>(\n type: string,\n data?: MessageData,\n fetchOptions?: {\n responseTimeout?: number\n signal?: AbortSignal\n suppressWarnings?: boolean\n },\n ): Promise<TResponse> => {\n return node.fetch(type, data, fetchOptions ?? {}) as Promise<TResponse>\n },\n [node],\n )\n return {\n sendMessage,\n fetch,\n }\n}\n","import * as React from 'react'\n\nconst MODULE_SLOT_KEY = Symbol.for('sanity.os.module')\n\ntype ModuleContext = React.Context<string | undefined>\ntype ModuleSlot = WeakMap<typeof React.createContext, ModuleContext>\n\n/**\n * Returns the React context that carries the current federation module id.\n *\n * @remarks\n * This is the slot the CLI-generated wrapper populates with\n * `renderOptions.moduleId`. The context lives in a per-React-copy slot on\n * `globalThis` so the CLI wrapper and the SDK share the same context even\n * across module copies. Each React copy gets its own context, since a context\n * created by one copy is inert in another.\n *\n * The slot is keyed on `React.createContext` rather than the `React` namespace\n * object: `import * as React` and `import React from 'react'` can yield\n * different wrapper objects for the same React copy under bundler interop,\n * whereas the `createContext` function is the same reference under both.\n *\n * The provider side lives in the CLI, which must not import the SDK:\n * `packages/@sanity/workbench-cli/src/actions/build/render-remote.ts` in\n * `sanity-io/cli` reconstructs this accessor (same symbol, same key) and\n * wraps `App` in the context's `Provider`. Nothing in this repo exports it.\n * @internal\n */\nexport function getDashboardModuleContext(): ModuleContext {\n const globals = globalThis as {[MODULE_SLOT_KEY]?: ModuleSlot}\n const slot = (globals[MODULE_SLOT_KEY] ??= new WeakMap())\n const key = React.createContext\n let context = slot.get(key)\n if (!context) {\n context = React.createContext<string | undefined>(undefined)\n slot.set(key, context)\n }\n return context\n}\n","import {type ClientError} from '@sanity/client'\nimport {AuthStateType, setAuthToken} from '@sanity/sdk'\nimport {getDashboardMessageBus} from '@sanity/sdk/_internal'\nimport {type MessageBus} from '@sanity/sdk/dashboard'\nimport React, {type PropsWithChildren, useContext, useEffect, useRef, useState} from 'react'\nimport {defer, of} from 'rxjs'\nimport {catchError} from 'rxjs/operators'\n\nimport {getDashboardModuleContext} from '../dashboard/module'\nimport {useAuthState} from '../hooks/auth/useAuthState'\nimport {useSanityInstance} from '../hooks/context/useSanityInstance'\n\n/**\n * Keeps the SDK auth token in sync with the dashboard \"OS\".\n *\n * When running inside the dashboard the OS owns the session, so we subscribe\n * to its `auth.token` stream and mirror each value into\n * the auth store — a token logs us in, `null` logs us out, and later OS\n * sign-in/out propagates automatically. When a request is rejected with a 401\n * (the token expired), we ask the OS to reissue rather than tearing the session\n * down; the new token arrives back through the same subscription.\n */\nfunction DashboardTokenRefresh({\n children,\n messageBus,\n}: PropsWithChildren<{messageBus: MessageBus}>) {\n const instance = useSanityInstance()\n const authState = useAuthState()\n const processed401ErrorRef = useRef<unknown | null>(null)\n\n useEffect(() => {\n const subscription = defer(() => messageBus.subscribe('auth.token'))\n .pipe(catchError(() => of(null)))\n .subscribe((token) => setAuthToken(instance, token))\n return () => subscription.unsubscribe()\n }, [instance, messageBus])\n\n useEffect(() => {\n const has401Error =\n authState.type === AuthStateType.ERROR && (authState.error as ClientError)?.statusCode === 401\n\n if (has401Error && processed401ErrorRef.current !== authState.error) {\n processed401ErrorRef.current = authState.error\n // Event topics have no replay, so a missing responder or timeout is otherwise dropped silently.\n messageBus.emit('auth.token.refresh', undefined).catch((error) => {\n // eslint-disable-next-line no-console\n console.warn('[sanity/sdk] Dashboard token refresh failed:', error)\n })\n } else if (!has401Error) {\n processed401ErrorRef.current = null\n }\n }, [authState, messageBus])\n\n return children\n}\n\n/**\n * Authenticates the SDK with the Sanity Dashboard's session when the app runs\n * inside the dashboard.\n *\n * The dashboard owns the session there: this provider subscribes to the token\n * the dashboard issues, writes each new value into the SDK's auth store (where\n * SDK hooks read it from), and asks the dashboard for a fresh token when a\n * request fails with a 401. Outside the dashboard it renders children\n * unchanged and the app's normal auth flow applies.\n *\n * @remarks\n * `AuthBoundary` mounts this automatically, so most apps never need it\n * directly. Mount it yourself only when your app runs inside the dashboard\n * without `AuthBoundary` — that is, the app renders its own loading and error\n * UI instead of the SDK's login flow — but still uses SDK hooks such as\n * `useQuery`, which need the dashboard's token in the auth store to\n * authenticate their requests.\n *\n * Mount it once, inside the provider that creates the Sanity instance whose\n * store should receive the token.\n *\n * @example\n * ```tsx\n * import {ResourceProvider} from '@sanity/sdk-react'\n * import {TokenRefreshProvider} from '@sanity/sdk-react/dashboard'\n *\n * function EmbeddedApp() {\n * return (\n * <ResourceProvider fallback={<Loading />}>\n * <TokenRefreshProvider>\n * <App />\n * </TokenRefreshProvider>\n * </ResourceProvider>\n * )\n * }\n * ```\n *\n * @public\n */\nexport const DashboardTokenRefreshProvider: React.FC<PropsWithChildren> = ({children}) => {\n const instance = useSanityInstance()\n const moduleId = useContext(getDashboardModuleContext())\n // The connection is first-caller-wins per instance, and hooks below read it during their\n // render, before any effect here could run. Connecting in the first render pins the module\n // identity before they do; nothing has subscribed to the store yet, so the write is safe.\n // The module id is read once: the CLI wrapper provides it statically above this tree.\n // No retry: the host installs the bus at module evaluation, before any remote renders, and\n // a standalone app has no host to wait for.\n const [messageBus] = useState(() => getDashboardMessageBus(instance, moduleId))\n if (messageBus) {\n return <DashboardTokenRefresh messageBus={messageBus}>{children}</DashboardTokenRefresh>\n }\n\n return children\n}\n","import {getDashboardOrganizationId, type OrganizationBase} from '@sanity/sdk'\nimport {getTopicState, isDashboardEnvironment} from '@sanity/sdk/_internal'\nimport {useMemo, useSyncExternalStore} from 'react'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\n\ntype CurrentOrganization = Pick<OrganizationBase, 'id' | 'name' | 'slug'> | null | undefined\n\n/**\n * @public\n *\n * A React hook that retrieves the dashboard organization ID that is currently selected in the Sanity Dashboard.\n *\n * Works in both Dashboard runtimes: it reads the `organizations.current` message bus topic when a\n * host has installed the bus, and falls back to the Comlink connection otherwise.\n *\n * @example\n * ```tsx\n * function DashboardComponent() {\n * const orgId = useOrganizationId()\n *\n * if (!orgId) return null\n *\n * return <div>Organization ID: {String(orgId)}</div>\n * }\n * ```\n *\n * @category Dashboard\n * @returns The dashboard organization ID (string | undefined)\n */\nexport function useOrganizationId(): string | undefined {\n const instance = useSanityInstance()\n const {subscribe, getCurrent} = useMemo(() => {\n if (!isDashboardEnvironment()) return getDashboardOrganizationId(instance)\n const source = getTopicState(instance, 'organizations.current')\n return {\n subscribe: source.subscribe,\n getCurrent: () => (source.getCurrent() as CurrentOrganization)?.id ?? undefined,\n }\n }, [instance])\n\n return useSyncExternalStore(subscribe, getCurrent)\n}\n","import {getTopicState, resolveTopic} from '@sanity/sdk/_internal'\nimport {type StateTopic, type TopicData} from '@sanity/sdk/dashboard'\n\nimport {createStateSourceHook} from '../helpers/createStateSourceHook'\n\n/**\n * Returns the current value of a dashboard state topic and follows later updates.\n *\n * The hook suspends until the topic publishes its first value, using the message bus query\n * deadline. A topic declared with `TopicResult` resolves to its successful value; a\n * failed result throws a `TopicError` to the nearest error boundary.\n *\n * @example\n * ```tsx\n * function ForegroundApplication() {\n * const foregroundId = useTopic('applications.foreground')\n * return <span>{foregroundId ?? 'No application in the foreground'}</span>\n * }\n * ```\n *\n * @public\n */\nexport const useTopic = createStateSourceHook({\n getState: getTopicState,\n // `getCurrent` throws a recorded read failure, which surfaces it from render like a thrown value.\n shouldSuspend: (instance, topic: StateTopic) =>\n getTopicState(instance, topic).getCurrent() === undefined,\n suspender: resolveTopic,\n}) as <K extends StateTopic>(topic: K) => TopicData<K>\n","/* eslint-disable react-compiler/react-compiler -- the transport branch in `useStudioWorkspacesByProjectIdDataset` is a deliberate rules-of-hooks exception; the compiler refuses files that disable it */\nimport {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'\nimport {getApplicationOrigin, isDashboardEnvironment} from '@sanity/sdk/_internal'\nimport {type TopicData} from '@sanity/sdk/dashboard'\nimport {useEffect, useMemo, useState} from 'react'\n\nimport {useWindowConnection} from '../comlink/useWindowConnection'\nimport {useTopic} from './useTopic'\n\nexport interface DashboardResource {\n id: string\n name: string\n title: string\n basePath: string\n projectId: string\n dataset: string\n type: string\n userApplicationId: string\n url: string\n}\n\ninterface WorkspacesByProjectIdDataset {\n [key: `${string}:${string}`]: DashboardResource[] // key format: `${projectId}:${dataset}`\n}\n\ninterface StudioWorkspacesResult {\n workspacesByProjectIdAndDataset: WorkspacesByProjectIdDataset\n error: string | null\n}\n\ntype DashboardApplications = NonNullable<TopicData<'applications.list'>>\n\n/**\n * Hook that fetches studio workspaces and organizes them by projectId:dataset\n *\n * Works in both Dashboard runtimes: it derives workspaces from the `applications.list` message\n * bus topic when a host has installed the bus, and falls back to the Comlink connection otherwise.\n * @internal\n *\n * @example\n * ```tsx\n * import {useStudioWorkspacesByProjectIdDataset} from '@sanity/sdk-react'\n * import {Card, Code, Button} from '@sanity/ui'\n * import {Suspense} from 'react'\n *\n * function WorkspacesCard() {\n * const {workspacesByProjectIdAndDataset, error} = useStudioWorkspacesByProjectIdDataset()\n * if (error) {\n * return <div>Error: {error}</div>\n * }\n * return (\n * <Card padding={4} radius={2} shadow={1}>\n * <Code language=\"json\">\n * {JSON.stringify(workspacesByProjectIdAndDataset, null, 2)}\n * </Code>\n * </Card>\n * )\n * }\n *\n * // Wrap the component with Suspense since the hook may suspend\n * function DashboardWorkspaces() {\n * return (\n * <Suspense fallback={<Button text=\"Loading...\" disabled />}>\n * <WorkspacesCard />\n * </Suspense>\n * )\n * }\n * ```\n */\nexport function useStudioWorkspacesByProjectIdDataset(): StudioWorkspacesResult {\n // The branch is stable: the transport is fixed for the page lifetime, so one set of hooks\n // always runs and the other never does.\n // eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime\n if (isDashboardEnvironment()) return useBusStudioWorkspaces()\n // eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime\n return useComlinkStudioWorkspaces()\n}\n\n// The legacy Comlink protocol models studios at the workspace level, so each workspace of a\n// studio's active deployment becomes one resource, addressed by the studio's origin.\nfunction toResources(application: DashboardApplications[number]): DashboardResource[] {\n if (application.type !== 'studio') return []\n const url = getApplicationOrigin(application) ?? ''\n return (application.activeDeployment?.workspaces ?? []).map((workspace) => ({\n id: workspace.id,\n name: workspace.name,\n title: workspace.title ?? application.title,\n basePath: workspace.basePath ?? '',\n projectId: workspace.projectId,\n dataset: workspace.dataset,\n type: 'studio',\n userApplicationId: application.id,\n url,\n }))\n}\n\nfunction toWorkspaceMap(applications: DashboardApplications): WorkspacesByProjectIdDataset {\n const workspaceMap: WorkspacesByProjectIdDataset = {}\n for (const resource of applications.flatMap(toResources)) {\n const key = `${resource.projectId}:${resource.dataset}` as const\n workspaceMap[key] ??= []\n workspaceMap[key].push(resource)\n }\n return workspaceMap\n}\n\n// Suspends until the host publishes its application list and throws a `TopicError` on failure,\n// like every bus hook, so `error` is always `null` on this path.\nfunction useBusStudioWorkspaces(): StudioWorkspacesResult {\n const applications = useTopic('applications.list')\n const workspacesByProjectIdAndDataset = useMemo(\n () => toWorkspaceMap(applications ?? []),\n [applications],\n )\n return {workspacesByProjectIdAndDataset, error: null}\n}\n\nfunction useComlinkStudioWorkspaces(): StudioWorkspacesResult {\n const [workspacesByProjectIdAndDataset, setWorkspacesByProjectIdAndDataset] =\n useState<WorkspacesByProjectIdDataset>({})\n const [error, setError] = useState<string | null>(null)\n\n const {fetch} = useWindowConnection({\n name: SDK_NODE_NAME,\n connectTo: SDK_CHANNEL_NAME,\n })\n\n // Once computed, this should probably be in a store and poll for changes\n // However, our stores are currently being refactored\n useEffect(() => {\n if (!fetch) return\n\n async function fetchWorkspaces(signal: AbortSignal) {\n try {\n const data = await fetch<{\n context: {availableResources: Array<DashboardResource>}\n }>('dashboard/v1/context', undefined, {signal})\n\n const workspaceMap: WorkspacesByProjectIdDataset = {}\n const noProjectIdAndDataset: DashboardResource[] = []\n\n data.context.availableResources.forEach((resource) => {\n if (resource.type !== 'studio') return\n if (!resource.projectId || !resource.dataset) {\n noProjectIdAndDataset.push(resource)\n return\n }\n const key = `${resource.projectId}:${resource.dataset}` as const\n if (!workspaceMap[key]) {\n workspaceMap[key] = []\n }\n workspaceMap[key].push(resource)\n })\n\n if (noProjectIdAndDataset.length > 0) {\n workspaceMap['NO_PROJECT_ID:NO_DATASET'] = noProjectIdAndDataset\n }\n\n setWorkspacesByProjectIdAndDataset(workspaceMap)\n setError(null)\n } catch (err: unknown) {\n if (err instanceof Error) {\n if (err.name === 'AbortError') {\n return\n }\n setError('Failed to fetch workspaces')\n }\n }\n }\n\n const controller = new AbortController()\n fetchWorkspaces(controller.signal)\n\n return () => {\n controller.abort()\n }\n }, [fetch])\n\n return {\n workspacesByProjectIdAndDataset,\n error,\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk-react",
3
- "version": "3.3.0-rc.0",
3
+ "version": "3.3.0",
4
4
  "private": false,
5
5
  "description": "Sanity SDK React toolkit for Content OS",
6
6
  "keywords": [
@@ -43,10 +43,10 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@module-federation/runtime": "^2.9.0",
46
- "@sanity/client": "^8.6.1",
46
+ "@sanity/client": "^8.6.2",
47
47
  "@sanity/message-protocol": "^0.24.0",
48
- "@sanity/sdk": "3.3.0-rc.0",
49
- "@sanity/types": "^6.13.2",
48
+ "@sanity/sdk": "3.3.0",
49
+ "@sanity/types": "^6.14.1",
50
50
  "react-compiler-runtime": "^1.0.0",
51
51
  "react-error-boundary": "^6.1.5",
52
52
  "rxjs": "^7.8.2"
@@ -62,23 +62,23 @@
62
62
  "@sanity/pkg-utils": "^13.0.0",
63
63
  "@testing-library/jest-dom": "^7.0.1",
64
64
  "@testing-library/react": "^16.3.3",
65
- "@types/node": "^24.13.4",
65
+ "@types/node": "^24.13.5",
66
66
  "@types/react": "^19.3.0",
67
67
  "@types/react-dom": "^19.3.0",
68
68
  "@vitejs/plugin-react": "^6.1.1",
69
- "@vitest/coverage-v8": "^5.0.0",
69
+ "@vitest/coverage-v8": "^5.0.1",
70
70
  "babel-plugin-react-compiler": "^1.0.0",
71
71
  "eslint": "^10.10.0",
72
72
  "groq": "^6.13.2",
73
73
  "groq-js": "^2.0.0",
74
74
  "jsdom": "^30.0.1",
75
- "oxfmt": "^0.67.0",
75
+ "oxfmt": "^0.68.0",
76
76
  "react": "^19.3.0",
77
77
  "react-dom": "^19.3.0",
78
78
  "rolldown": "^1.2.8",
79
79
  "typescript": "^6.0.3",
80
80
  "vite": "^8.3.0",
81
- "vitest": "^5.0.0",
81
+ "vitest": "^5.0.1",
82
82
  "xstate": "5.32.6"
83
83
  },
84
84
  "peerDependencies": {
@@ -1 +1,6 @@
1
- export {installMessageBus, resetMessageBus} from '@sanity/sdk/_internal'
1
+ export {
2
+ type ApplicationStatus,
3
+ type ApplicationStatusUpdate,
4
+ installMessageBus,
5
+ resetMessageBus,
6
+ } from '@sanity/sdk/_internal'
@@ -10,6 +10,7 @@ import {
10
10
  type MessageBusEmitResult,
11
11
  type MessageBusError,
12
12
  type MessageBusErrorCode,
13
+ type MessageBusHost,
13
14
  type MessageBusMessage,
14
15
  type MessageBusMeta,
15
16
  type MessageBusQueryOptions,
@@ -18,8 +19,14 @@ import {
18
19
  type StateTopicDef,
19
20
  type TopicName,
20
21
  type Topics,
22
+ useApplicationBasePath,
21
23
  } from './dashboard'
22
- import {type installMessageBus, type resetMessageBus} from './dashboard-internal'
24
+ import {
25
+ type ApplicationStatus,
26
+ type ApplicationStatusUpdate,
27
+ type installMessageBus,
28
+ type resetMessageBus,
29
+ } from './dashboard-internal'
23
30
 
24
31
  // Guards issue #1: every `@public` message bus type must be reachable from the
25
32
  // `@sanity/sdk-react/dashboard` entrypoint. A missing export fails `ts:check`
@@ -54,11 +61,30 @@ test('dashboard entrypoint exposes the message bus public types', () => {
54
61
  expectTypeOf<MessageBusQueryOptions['timeout']>().toEqualTypeOf<number | null | undefined>()
55
62
  expectTypeOf<MessageBusEmitResult<string>>().toExtend<PromiseLike<string>>()
56
63
  expectTypeOf<MessageBus['query']>().toBeFunction()
64
+ expectTypeOf<ReturnType<typeof useApplicationBasePath>>().toEqualTypeOf<string>()
65
+ expectTypeOf<MessageBusHost['connections']['subscribe']>().toBeFunction()
57
66
  })
58
67
 
59
- // Guards issue #3: the test-isolation helpers stay on the internal entrypoint,
60
- // not the public one.
61
- test('internal entrypoint exposes install and reset', () => {
68
+ test('internal entrypoint exposes dashboard internals', () => {
69
+ expectTypeOf<ApplicationStatus>().toEqualTypeOf<{label: string | null}>()
70
+ expectTypeOf<ApplicationStatusUpdate>().toEqualTypeOf<{
71
+ name: string
72
+ value: ApplicationStatus
73
+ }>()
74
+ expectTypeOf<PayloadOf<'applications.status.update'>>().toEqualTypeOf<ApplicationStatusUpdate>()
75
+ const emitApplicationStatus = (messageBus: ReturnType<typeof installMessageBus>) => {
76
+ messageBus.emit('applications.status.update', {name: 'list', value: {label: null}})
77
+ // @ts-expect-error application statuses always carry a label field
78
+ messageBus.emit('applications.status.update', {name: 'list', value: null})
79
+ }
80
+ const subscribeToApplicationStatus = (messageBus: ReturnType<typeof installMessageBus>) => {
81
+ messageBus.subscribe('applications.status.update')
82
+ messageBus.subscribe('applications.status.update', (message) => {
83
+ expectTypeOf(message.type).toEqualTypeOf<'applications.status.update'>()
84
+ })
85
+ }
86
+ expectTypeOf(emitApplicationStatus).toBeFunction()
87
+ expectTypeOf(subscribeToApplicationStatus).toBeFunction()
62
88
  expectTypeOf<typeof installMessageBus>().toBeFunction()
63
89
  expectTypeOf<typeof resetMessageBus>().toBeFunction()
64
90
  })
@@ -25,6 +25,7 @@ export {
25
25
  useAgentResourceContext,
26
26
  } from '../hooks/dashboard/useAgentResourceContext'
27
27
  export {useApplication} from '../hooks/dashboard/useApplication'
28
+ export {useApplicationBasePath} from '../hooks/dashboard/useApplicationBasePath'
28
29
  export {
29
30
  type ApplicationConfigSelector,
30
31
  useApplicationConfig,
@@ -37,6 +38,8 @@ export {
37
38
  type DashboardWebWorker,
38
39
  useApplications,
39
40
  } from '../hooks/dashboard/useApplications'
41
+ export {useAuthToken} from '../hooks/dashboard/useAuthToken'
42
+ export {useCurrentUser} from '../hooks/dashboard/useCurrentUser'
40
43
  export {type TopicEmitter, useEmit} from '../hooks/dashboard/useEmit'
41
44
  export {useNavigate} from '../hooks/dashboard/useNavigate'
42
45
  export {
@@ -29,11 +29,8 @@ export {useAuthState} from '../hooks/auth/useAuthState'
29
29
  export {useAuthToken} from '../hooks/auth/useAuthToken'
30
30
  export {useCurrentUser} from '../hooks/auth/useCurrentUser'
31
31
  export {useHandleAuthCallback} from '../hooks/auth/useHandleAuthCallback'
32
- export {useHandleOAuthCallback} from '../hooks/auth/useHandleOAuthCallback'
33
32
  export {useLoginUrl} from '../hooks/auth/useLoginUrl'
34
33
  export {useLogOut} from '../hooks/auth/useLogOut'
35
- export {useOAuthAuthorize} from '../hooks/auth/useOAuthAuthorize'
36
- export {useOAuthTokens, type UseOAuthTokensResult} from '../hooks/auth/useOAuthTokens'
37
34
  export {useVerifyOrgProjects} from '../hooks/auth/useVerifyOrgProjects'
38
35
  export {useClient} from '../hooks/client/useClient'
39
36
  export {
@@ -9,7 +9,6 @@ import {DashboardTokenRefreshProvider} from '../../context/DashboardTokenRefresh
9
9
  import {ResourceProvider} from '../../context/ResourceProvider'
10
10
  import {useAuthState} from '../../hooks/auth/useAuthState'
11
11
  import {useLoginUrl} from '../../hooks/auth/useLoginUrl'
12
- import {useOAuthAuthorize} from '../../hooks/auth/useOAuthAuthorize'
13
12
  import {useVerifyOrgProjects} from '../../hooks/auth/useVerifyOrgProjects'
14
13
  import {AuthBoundary} from './AuthBoundary'
15
14
 
@@ -18,9 +17,6 @@ vi.mock('../../hooks/auth/useAuthState', () => ({
18
17
  useAuthState: vi.fn(() => 'logged-out'),
19
18
  }))
20
19
  vi.mock('../../hooks/auth/useLoginUrl')
21
- vi.mock('../../hooks/auth/useOAuthAuthorize', () => ({
22
- useOAuthAuthorize: vi.fn(() => vi.fn().mockResolvedValue(undefined)),
23
- }))
24
20
  vi.mock('../../hooks/auth/useVerifyOrgProjects')
25
21
  vi.mock('../../hooks/auth/useHandleAuthCallback', () => ({
26
22
  useHandleAuthCallback: vi.fn(() => async () => {}),
@@ -38,8 +34,8 @@ vi.mock('./AuthError', async (importOriginal) => {
38
34
  return {
39
35
  ...actual,
40
36
  AuthError: class MockAuthError extends Error {
41
- constructor(error: unknown) {
42
- super(error instanceof Error ? error.message : undefined)
37
+ constructor(error: Error) {
38
+ super(error.message)
43
39
  this.name = 'AuthError'
44
40
  this.cause = error
45
41
  }
@@ -180,81 +176,6 @@ describe('AuthBoundary', () => {
180
176
  }
181
177
  })
182
178
 
183
- describe('oauth mode', () => {
184
- const oauth = {
185
- clientId: 'client-abc',
186
- redirectUri: 'https://app.example.com/callback',
187
- organizationId: 'org123',
188
- }
189
-
190
- it('starts the OAuth authorization flow when authState="logged-out"', async () => {
191
- const authorize = vi.fn().mockResolvedValue(undefined)
192
- vi.mocked(useOAuthAuthorize).mockReturnValue(authorize)
193
- vi.mocked(useAuthState).mockReturnValue({
194
- type: AuthStateType.LOGGED_OUT,
195
- isDestroyingSession: false,
196
- })
197
- render(
198
- <ResourceProvider projectId="p" dataset="d" auth={{oauth}} fallback={null}>
199
- <AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
200
- </ResourceProvider>,
201
- )
202
-
203
- await waitFor(() => expect(authorize).toHaveBeenCalledTimes(1))
204
- expect(screen.queryByText('Protected Content')).not.toBeInTheDocument()
205
- })
206
-
207
- it('does not start the OAuth flow when logged out without oauth config', async () => {
208
- const authorize = vi.fn().mockResolvedValue(undefined)
209
- vi.mocked(useOAuthAuthorize).mockReturnValue(authorize)
210
- vi.mocked(useAuthState).mockReturnValue({
211
- type: AuthStateType.LOGGED_OUT,
212
- isDestroyingSession: false,
213
- })
214
- render(
215
- <ResourceProvider projectId="p" dataset="d" fallback={null}>
216
- <AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
217
- </ResourceProvider>,
218
- )
219
-
220
- await waitFor(() => expect(screen.queryByText('Protected Content')).not.toBeInTheDocument())
221
- expect(authorize).not.toHaveBeenCalled()
222
- })
223
-
224
- it('renders the error fallback when starting the OAuth flow rejects', async () => {
225
- // A falsy rejection reason must still surface as an error, not a blank screen.
226
- vi.mocked(useOAuthAuthorize).mockReturnValue(vi.fn().mockRejectedValue(undefined))
227
- vi.mocked(useAuthState).mockReturnValue({
228
- type: AuthStateType.LOGGED_OUT,
229
- isDestroyingSession: false,
230
- })
231
- render(
232
- <ResourceProvider projectId="p" dataset="d" auth={{oauth}} fallback={null}>
233
- <AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
234
- </ResourceProvider>,
235
- )
236
-
237
- await waitFor(() => expect(screen.getByText('Authentication Error')).toBeInTheDocument())
238
- })
239
-
240
- it('renders the error fallback without restarting the flow when authState="error"', async () => {
241
- const authorize = vi.fn().mockResolvedValue(undefined)
242
- vi.mocked(useOAuthAuthorize).mockReturnValue(authorize)
243
- vi.mocked(useAuthState).mockReturnValue({
244
- type: AuthStateType.ERROR,
245
- error: new Error('access_denied'),
246
- })
247
- render(
248
- <ResourceProvider projectId="p" dataset="d" auth={{oauth}} fallback={null}>
249
- <AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
250
- </ResourceProvider>,
251
- )
252
-
253
- await waitFor(() => expect(screen.getByText('Authentication Error')).toBeInTheDocument())
254
- expect(authorize).not.toHaveBeenCalled()
255
- })
256
- })
257
-
258
179
  it('renders the empty LoginCallback component when authState="logging-in"', () => {
259
180
  vi.mocked(useAuthState).mockReturnValue({
260
181
  type: AuthStateType.LOGGING_IN,
@@ -1,14 +1,13 @@
1
1
  import {CorsOriginError} from '@sanity/client'
2
2
  import {AuthStateType, getCorsErrorProjectId, isImportError} from '@sanity/sdk'
3
3
  import {isDashboardEnvironment, isStudioConfig} from '@sanity/sdk/_internal'
4
- import {useEffect, useMemo, useState} from 'react'
4
+ import {useEffect, useMemo} from 'react'
5
5
  import {ErrorBoundary, type FallbackProps} from 'react-error-boundary'
6
6
 
7
7
  import {ComlinkTokenRefreshProvider} from '../../context/ComlinkTokenRefresh'
8
8
  import {DashboardTokenRefreshProvider} from '../../context/DashboardTokenRefresh'
9
9
  import {useAuthState} from '../../hooks/auth/useAuthState'
10
10
  import {useLoginUrl} from '../../hooks/auth/useLoginUrl'
11
- import {useOAuthAuthorize} from '../../hooks/auth/useOAuthAuthorize'
12
11
  import {useVerifyOrgProjects} from '../../hooks/auth/useVerifyOrgProjects'
13
12
  import {useSanityInstance} from '../../hooks/context/useSanityInstance'
14
13
  import {ChunkLoadError} from '../errors/ChunkLoadError'
@@ -184,34 +183,21 @@ function AuthSwitch({
184
183
  const orgError = useVerifyOrgProjects(disableVerifyOrg, projectIds)
185
184
 
186
185
  const isLoggedOut = authState.type === AuthStateType.LOGGED_OUT && !authState.isDestroyingSession
187
- const isOAuth = !!instance.config.auth?.oauth
188
186
  const loginUrl = useLoginUrl()
189
- const authorize = useOAuthAuthorize()
190
- const [authorizeError, setAuthorizeError] = useState<{error: unknown} | null>(null)
191
187
 
192
188
  useEffect(() => {
193
189
  if (isLoggedOut && !isInIframe() && !isStudio && !isDashboardEnvironment()) {
194
190
  // We don't want to redirect to login if we're in the Dashboard, in studio
195
191
  // mode, or in the workbench (the OS owns the session and mints the token)
196
- if (isOAuth) {
197
- // PKCE params and navigation are owned by core. LOGGED_OUT renders
198
- // null, so a rejection here must be surfaced or the user sees nothing.
199
- authorize().catch((error) => setAuthorizeError({error}))
200
- } else {
201
- window.location.href = loginUrl
202
- }
192
+ window.location.href = loginUrl
203
193
  }
204
- }, [isLoggedOut, isOAuth, authorize, loginUrl, isStudio])
194
+ }, [isLoggedOut, loginUrl, isStudio])
205
195
 
206
196
  // Only check the error if verification is enabled
207
197
  if (verifyOrganization && orgError) {
208
198
  throw new ConfigurationError({message: orgError})
209
199
  }
210
200
 
211
- if (authorizeError) {
212
- throw new AuthError(authorizeError.error)
213
- }
214
-
215
201
  switch (authState.type) {
216
202
  case AuthStateType.ERROR: {
217
203
  throw new AuthError(authState.error)
@@ -9,14 +9,7 @@ vi.mock('../../hooks/auth/useHandleAuthCallback', () => ({
9
9
  const parsedUrl = new URL(url)
10
10
  const sid = new URLSearchParams(parsedUrl.hash.slice(1)).get('sid')
11
11
  if (sid === 'valid') {
12
- // same document, hash stripped
13
- return 'http://localhost/'
14
- }
15
- if (sid === 'deep-link') {
16
- return 'http://localhost/documents/abc?x=1'
17
- }
18
- if (sid === 'cross-origin') {
19
- return 'https://evil.example.com/'
12
+ return 'https://example.com/new-location'
20
13
  }
21
14
  return false
22
15
  }),
@@ -55,25 +48,7 @@ describe('LoginCallback', () => {
55
48
 
56
49
  it('handles a successful callback and calls history.replaceState', async () => {
57
50
  // Simulate a valid `sid` in the location hash
58
- const replace = vi.fn()
59
- vi.stubGlobal('location', {href: 'http://localhost/#sid=valid', replace})
60
- const {LoginCallback} = await import('./LoginCallback') // Reload after resetModules
61
-
62
- render(
63
- <ResourceProvider fallback={null}>
64
- <LoginCallback />
65
- </ResourceProvider>,
66
- )
67
-
68
- await waitFor(() => {
69
- expect(history.replaceState).toHaveBeenCalledWith(null, '', 'http://localhost/')
70
- })
71
- expect(replace).not.toHaveBeenCalled()
72
- })
73
-
74
- it('navigates when the callback resolves to a different route', async () => {
75
- const replace = vi.fn()
76
- vi.stubGlobal('location', {href: 'http://localhost/#sid=deep-link', replace})
51
+ vi.stubGlobal('location', {href: 'http://localhost#sid=valid'})
77
52
  const {LoginCallback} = await import('./LoginCallback') // Reload after resetModules
78
53
 
79
54
  render(
@@ -83,25 +58,11 @@ describe('LoginCallback', () => {
83
58
  )
84
59
 
85
60
  await waitFor(() => {
86
- expect(replace).toHaveBeenCalledWith('http://localhost/documents/abc?x=1')
87
- })
88
- expect(history.replaceState).not.toHaveBeenCalled()
89
- })
90
-
91
- it('does not navigate when the callback resolves to a different origin', async () => {
92
- const replace = vi.fn()
93
- vi.stubGlobal('location', {href: 'http://localhost/#sid=cross-origin', replace})
94
- const {LoginCallback} = await import('./LoginCallback') // Reload after resetModules
95
-
96
- render(
97
- <ResourceProvider fallback={null}>
98
- <LoginCallback />
99
- </ResourceProvider>,
100
- )
101
-
102
- await waitFor(() => {
103
- expect(replace).not.toHaveBeenCalled()
104
- expect(history.replaceState).not.toHaveBeenCalled()
61
+ expect(history.replaceState).toHaveBeenCalledWith(
62
+ null,
63
+ '',
64
+ 'https://example.com/new-location',
65
+ )
105
66
  })
106
67
  })
107
68
 
@@ -5,15 +5,7 @@ import {useHandleAuthCallback} from '../../hooks/auth/useHandleAuthCallback'
5
5
  /**
6
6
  * Component shown during auth callback processing that handles login completion.
7
7
  * Automatically processes the auth callback when mounted and updates the URL
8
- * to remove callback parameters without triggering a page reload. When the
9
- * callback resolves to a different route (the OAuth flow returns the user to
10
- * where they started), a real navigation is performed instead so the app's
11
- * router picks it up.
12
- *
13
- * A different route is detected by pathname only, so apps that route in the
14
- * hash (`#/documents/abc`) will not be navigated to the deep link. Those apps
15
- * should build a custom callback component with `useHandleOAuthCallback` and
16
- * their router's `navigate`.
8
+ * to remove callback parameters without triggering a page reload.
17
9
  *
18
10
  * @alpha
19
11
  */
@@ -23,20 +15,10 @@ export function LoginCallback(): React.ReactNode {
23
15
  useEffect(() => {
24
16
  const url = new URL(location.href)
25
17
  handleAuthCallback(url.toString()).then((replacementLocation) => {
26
- if (!replacementLocation) return
27
- const next = new URL(replacementLocation, url)
28
- // Core only returns same-origin locations; guard here too since this is
29
- // the code that navigates.
30
- if (next.origin !== url.origin) return
31
- if (next.pathname === url.pathname) {
32
- // Same document: `replaceState` strips the callback params without a
33
- // reload. Routers do not observe this, which is fine when only the
34
- // query/hash changed. Caveat: a same-path return with different app
35
- // query params won't re-run router search-param hooks until the next
36
- // navigation.
18
+ if (replacementLocation) {
19
+ // history API with `replaceState` is used to prevent a reload but still
20
+ // remove the short-lived token from the URL
37
21
  history.replaceState(null, '', replacementLocation)
38
- } else {
39
- location.replace(replacementLocation)
40
22
  }
41
23
  })
42
24
  }, [handleAuthCallback])