diff --git a/packages/core/src/_exports/index.ts b/packages/core/src/_exports/index.ts index 2b342664..e3df1f00 100644 --- a/packages/core/src/_exports/index.ts +++ b/packages/core/src/_exports/index.ts @@ -24,6 +24,13 @@ export { export {observeOrganizationVerificationState} from '../auth/getOrganizationVerificationState' export {handleAuthCallback} from '../auth/handleAuthCallback' export {logout} from '../auth/logout' +export { + type ApiErrorBody, + getClientErrorApiBody, + getClientErrorApiDescription, + getClientErrorApiType, + isProjectUserNotFoundClientError, +} from '../auth/utils' export type {ClientStoreState as ClientState} from '../client/clientStore' export {type ClientOptions, getClient, getClientState} from '../client/clientStore' export { diff --git a/packages/core/src/auth/utils.ts b/packages/core/src/auth/utils.ts index c04e6210..92c9e2b7 100644 --- a/packages/core/src/auth/utils.ts +++ b/packages/core/src/auth/utils.ts @@ -1,3 +1,4 @@ +import {type ClientError} from '@sanity/client' import {EMPTY, fromEvent, Observable} from 'rxjs' import {AUTH_CODE_PARAM, DEFAULT_BASE} from './authConstants' @@ -134,3 +135,38 @@ export function getCleanedUrl(locationUrl: string): string { loc.searchParams.delete('url') return loc.toString() } + +// ----------------------------------------------------------------------------- +// ClientError helpers (shared) +// ----------------------------------------------------------------------------- + +/** @internal */ +export type ApiErrorBody = { + error?: {type?: string; description?: string} + type?: string + description?: string + message?: string +} + +/** @internal Extracts the structured API error body from a ClientError, if present. */ +export function getClientErrorApiBody(error: ClientError): ApiErrorBody | undefined { + const body: unknown = (error as ClientError).response?.body + return body && typeof body === 'object' ? (body as ApiErrorBody) : undefined +} + +/** @internal Returns the error type string from an API error body, if available. */ +export function getClientErrorApiType(error: ClientError): string | undefined { + const body = getClientErrorApiBody(error) + return body?.error?.type ?? body?.type +} + +/** @internal Returns the error description string from an API error body, if available. */ +export function getClientErrorApiDescription(error: ClientError): string | undefined { + const body = getClientErrorApiBody(error) + return body?.error?.description ?? body?.description +} + +/** @internal True if the error represents a projectUserNotFoundError. */ +export function isProjectUserNotFoundClientError(error: ClientError): boolean { + return getClientErrorApiType(error) === 'projectUserNotFoundError' +} diff --git a/packages/react/src/components/auth/LoginError.tsx b/packages/react/src/components/auth/LoginError.tsx index 455028ca..b6bd6df9 100644 --- a/packages/react/src/components/auth/LoginError.tsx +++ b/packages/react/src/components/auth/LoginError.tsx @@ -1,5 +1,10 @@ import {ClientError} from '@sanity/client' -import {AuthStateType} from '@sanity/sdk' +import { + AuthStateType, + getClientErrorApiBody, + getClientErrorApiDescription, + isProjectUserNotFoundClientError, +} from '@sanity/sdk' import {useCallback, useEffect, useState} from 'react' import {type FallbackProps} from 'react-error-boundary' @@ -35,6 +40,7 @@ export function LoginError({error, resetErrorBoundary}: LoginErrorProps): React. const [authErrorMessage, setAuthErrorMessage] = useState( 'Please try again or contact support if the problem persists.', ) + const [showRetryCta, setShowRetryCta] = useState(true) const handleRetry = useCallback(async () => { await logout() @@ -44,18 +50,28 @@ export function LoginError({error, resetErrorBoundary}: LoginErrorProps): React. useEffect(() => { if (error instanceof ClientError) { if (error.statusCode === 401) { - handleRetry() + // Surface a friendly message for projectUserNotFoundError (do not logout/refresh) + if (isProjectUserNotFoundClientError(error)) { + const description = getClientErrorApiDescription(error) + if (description) setAuthErrorMessage(description) + setShowRetryCta(false) + } else { + setShowRetryCta(true) + handleRetry() + } } else if (error.statusCode === 404) { - const errorMessage = error.response.body.message || '' + const errorMessage = getClientErrorApiBody(error)?.message || '' if (errorMessage.startsWith('Session with sid') && errorMessage.endsWith('not found')) { setAuthErrorMessage('The session ID is invalid or expired.') } else { setAuthErrorMessage('The login link is invalid or expired. Please try again.') } + setShowRetryCta(true) } } if (authState.type !== AuthStateType.ERROR && error instanceof ConfigurationError) { setAuthErrorMessage(error.message) + setShowRetryCta(true) } }, [authState, handleRetry, error]) @@ -63,10 +79,14 @@ export function LoginError({error, resetErrorBoundary}: LoginErrorProps): React. ) }