diff --git a/icons/error-pages/403.svg b/icons/error-pages/403.svg new file mode 100644 index 0000000000..d4369a0a61 --- /dev/null +++ b/icons/error-pages/403.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/name.d.ts b/public/icons/name.d.ts index 4126633c3e..a0d663bdde 100644 --- a/public/icons/name.d.ts +++ b/public/icons/name.d.ts @@ -55,6 +55,7 @@ | "empty_search_result" | "ENS_slim" | "ENS" + | "error-pages/403" | "error-pages/404" | "error-pages/422" | "error-pages/429" diff --git a/ui/address/utils/useAddressQuery.ts b/ui/address/utils/useAddressQuery.ts index 3f1ae73929..403e568ddc 100644 --- a/ui/address/utils/useAddressQuery.ts +++ b/ui/address/utils/useAddressQuery.ts @@ -25,6 +25,8 @@ interface Params { isEnabled?: boolean; } +const NO_RPC_FALLBACK_ERROR_CODES = [ 403 ]; + export default function useAddressQuery({ hash, isEnabled = true }: Params): AddressQuery { const [ isRefetchEnabled, setRefetchEnabled ] = React.useState(false); @@ -35,6 +37,10 @@ export default function useAddressQuery({ hash, isEnabled = true }: Params): Add placeholderData: ADDRESS_INFO, refetchOnMount: false, retry: (failureCount, error) => { + if (error.status < 500) { + return false; + } + if (isRefetchEnabled) { return false; } @@ -92,7 +98,7 @@ export default function useAddressQuery({ hash, isEnabled = true }: Params): Add }; }, placeholderData: [ GET_BALANCE ], - enabled: apiQuery.isError || apiQuery.errorUpdateCount > 0, + enabled: (apiQuery.isError || apiQuery.errorUpdateCount > 0) && !NO_RPC_FALLBACK_ERROR_CODES.includes(apiQuery.error?.status ?? 999), retry: false, refetchOnMount: false, }); @@ -107,7 +113,7 @@ export default function useAddressQuery({ hash, isEnabled = true }: Params): Add } else if (!apiQuery.isError) { setRefetchEnabled(false); } - }, [ apiQuery.errorUpdateCount, apiQuery.isError, apiQuery.isPlaceholderData ]); + }, [ apiQuery.errorUpdateCount, apiQuery.isError, apiQuery.isPlaceholderData, apiQuery.error?.status ]); React.useEffect(() => { if (!rpcQuery.isPlaceholderData && !rpcQuery.data) { @@ -115,7 +121,14 @@ export default function useAddressQuery({ hash, isEnabled = true }: Params): Add } }, [ rpcQuery.data, rpcQuery.isPlaceholderData ]); - const isRpcQuery = Boolean((apiQuery.isError || apiQuery.isPlaceholderData) && apiQuery.errorUpdateCount > 0 && rpcQuery.data && publicClient); + const isRpcQuery = Boolean( + (apiQuery.isError || apiQuery.isPlaceholderData) && + !NO_RPC_FALLBACK_ERROR_CODES.includes(apiQuery.error?.status ?? 999) && + apiQuery.errorUpdateCount > 0 && + rpcQuery.data && + publicClient, + ); + const query = isRpcQuery ? rpcQuery as UseQueryResult> : apiQuery; return { diff --git a/ui/shared/AppError/AppError.pw.tsx b/ui/shared/AppError/AppError.pw.tsx index c4ec388e37..0cd6acbdca 100644 --- a/ui/shared/AppError/AppError.pw.tsx +++ b/ui/shared/AppError/AppError.pw.tsx @@ -20,6 +20,12 @@ test('status code 422', async({ render }) => { await expect(component).toHaveScreenshot(); }); +test('status code 403', async({ render }) => { + const error = { message: 'Test', cause: { status: 403 } } as Error; + const component = await render(); + await expect(component).toHaveScreenshot(); +}); + test('status code 500', async({ render }) => { const error = { message: 'Unknown error', cause: { status: 500 } } as Error; const component = await render(); diff --git a/ui/shared/AppError/AppError.tsx b/ui/shared/AppError/AppError.tsx index aca50cf4d0..c93786ba1b 100644 --- a/ui/shared/AppError/AppError.tsx +++ b/ui/shared/AppError/AppError.tsx @@ -24,6 +24,10 @@ interface Props { } const ERROR_TEXTS: Record = { + '403': { + title: 'Forbidden', + text: 'Access to this resource is restricted.', + }, '404': { title: 'Page not found', text: 'This page is no longer explorable! If you are lost, use the search bar to find what you are looking for.', diff --git a/ui/shared/AppError/AppErrorIcon.tsx b/ui/shared/AppError/AppErrorIcon.tsx index 55b69cd1d1..5ada344b29 100644 --- a/ui/shared/AppError/AppErrorIcon.tsx +++ b/ui/shared/AppError/AppErrorIcon.tsx @@ -4,6 +4,7 @@ import type { IconName } from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg'; const ICONS: Record = { + '403': 'error-pages/403', '404': 'error-pages/404', '422': 'error-pages/422', '429': 'error-pages/429', diff --git a/ui/shared/AppError/__screenshots__/AppError.pw.tsx_default_status-code-403-1.png b/ui/shared/AppError/__screenshots__/AppError.pw.tsx_default_status-code-403-1.png new file mode 100644 index 0000000000..512639aa5c Binary files /dev/null and b/ui/shared/AppError/__screenshots__/AppError.pw.tsx_default_status-code-403-1.png differ diff --git a/ui/shared/AppError/isCustomAppError.ts b/ui/shared/AppError/isCustomAppError.ts index 328368e2ae..462de0cfd1 100644 --- a/ui/shared/AppError/isCustomAppError.ts +++ b/ui/shared/AppError/isCustomAppError.ts @@ -1,7 +1,7 @@ import type { ResourceError } from 'lib/api/resources'; // status codes when custom error screen should be shown -const CUSTOM_STATUS_CODES = [ 404, 422, 429 ]; +const CUSTOM_STATUS_CODES = [ 403, 404, 422, 429 ]; export default function isCustomAppError(error: ResourceError) { return CUSTOM_STATUS_CODES.includes(error.status);