diff --git a/packages/console/src/App.tsx b/packages/console/src/App.tsx index b3261f093..edcbb41c9 100644 --- a/packages/console/src/App.tsx +++ b/packages/console/src/App.tsx @@ -17,6 +17,7 @@ import initI18n from '@/i18n/init'; import { adminTenantEndpoint } from './consts'; import { isCloud } from './consts/cloud'; +import ErrorBoundary from './containers/ErrorBoundary'; import AppConfirmModalProvider from './contexts/AppConfirmModalProvider'; import AppEndpointsProvider from './contexts/AppEndpointsProvider'; import TenantsProvider, { TenantsContext } from './contexts/TenantsProvider'; @@ -68,9 +69,11 @@ const Content = () => { const App = () => { return ( - - - + + + + + ); }; export default App; diff --git a/packages/console/src/components/AppError/index.tsx b/packages/console/src/components/AppError/index.tsx index c97d0d35c..38e3f6aae 100644 --- a/packages/console/src/components/AppError/index.tsx +++ b/packages/console/src/components/AppError/index.tsx @@ -6,8 +6,8 @@ import ErrorDark from '@/assets/images/error-dark.svg'; import Error from '@/assets/images/error.svg'; import KeyboardArrowDown from '@/assets/images/keyboard-arrow-down.svg'; import KeyboardArrowUp from '@/assets/images/keyboard-arrow-up.svg'; -import { useTheme } from '@/hooks/use-theme'; import { onKeyDownHandler } from '@/utils/a11y'; +import { getThemeFromLocalStorage } from '@/utils/theme'; import * as styles from './index.module.scss'; @@ -22,7 +22,7 @@ type Props = { const AppError = ({ title, errorCode, errorMessage, callStack, children }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const [isDetailsOpen, setIsDetailsOpen] = useState(false); - const theme = useTheme(); + const theme = getThemeFromLocalStorage(); // Should be able to use the component in an offline environment return (
diff --git a/packages/console/src/components/AppLoading/Offline.tsx b/packages/console/src/components/AppLoading/Offline.tsx index 88a13fdea..12e3b0f5a 100644 --- a/packages/console/src/components/AppLoading/Offline.tsx +++ b/packages/console/src/components/AppLoading/Offline.tsx @@ -1,12 +1,7 @@ -import { AppearanceMode } from '@logto/schemas'; -import { trySafe } from '@silverhand/essentials'; -import { z } from 'zod'; - import IllustrationDark from '@/assets/images/loading-illustration-dark.svg'; import Illustration from '@/assets/images/loading-illustration.svg'; import { Daisy as Spinner } from '@/components/Spinner'; -import { themeStorageKey } from '@/consts'; -import { getTheme } from '@/utils/theme'; +import { getThemeFromLocalStorage } from '@/utils/theme'; import * as styles from './index.module.scss'; @@ -14,10 +9,7 @@ import * as styles from './index.module.scss'; * An fullscreen loading component fetches local stored theme without sending request. */ export const AppLoadingOffline = () => { - const theme = getTheme( - trySafe(() => z.nativeEnum(AppearanceMode).parse(localStorage.getItem(themeStorageKey))) ?? - AppearanceMode.SyncWithSystem - ); + const theme = getThemeFromLocalStorage(); return (
diff --git a/packages/console/src/containers/ErrorBoundary/index.tsx b/packages/console/src/containers/ErrorBoundary/index.tsx index e2fbdf779..7f5a75de9 100644 --- a/packages/console/src/containers/ErrorBoundary/index.tsx +++ b/packages/console/src/containers/ErrorBoundary/index.tsx @@ -16,10 +16,8 @@ type State = { }; class ErrorBoundary extends Component { - static getDerivedStateFromError(error: Error) { - const errorMessage = conditional( - typeof error === 'object' && typeof error.message === 'string' && error.message - ); + static getDerivedStateFromError(error: Error): State { + const errorMessage = String(error); const callStack = conditional( typeof error === 'object' && @@ -36,6 +34,24 @@ class ErrorBoundary extends Component { hasError: false, }; + promiseRejectionHandler(error: unknown) { + this.setState( + ErrorBoundary.getDerivedStateFromError( + error instanceof Error ? error : new Error(String(error)) + ) + ); + } + + componentDidMount(): void { + window.addEventListener('unhandledrejection', (event) => { + this.promiseRejectionHandler(event.reason); + }); + } + + componentWillUnmount(): void { + window.removeEventListener('unhandledrejection', this.promiseRejectionHandler); + } + render() { const { children } = this.props; const { callStack, errorMessage, hasError } = this.state; diff --git a/packages/console/src/hooks/use-swr-options.ts b/packages/console/src/hooks/use-swr-options.ts index cb2c0a0fe..486640dd8 100644 --- a/packages/console/src/hooks/use-swr-options.ts +++ b/packages/console/src/hooks/use-swr-options.ts @@ -4,7 +4,7 @@ import useApi, { RequestError } from './use-api'; import useSwrFetcher from './use-swr-fetcher'; const useSwrOptions = (): SWRConfiguration => { - const api = useApi({ hideErrorToast: true }); + const api = useApi(); const fetcher = useSwrFetcher(api); return { diff --git a/packages/console/src/utils/theme.ts b/packages/console/src/utils/theme.ts index b1598ea51..f4590be46 100644 --- a/packages/console/src/utils/theme.ts +++ b/packages/console/src/utils/theme.ts @@ -1,4 +1,8 @@ import { AppearanceMode } from '@logto/schemas'; +import { trySafe } from '@silverhand/essentials'; +import { z } from 'zod'; + +import { themeStorageKey } from '@/consts'; export const getTheme = (appearanceMode: AppearanceMode) => { if (appearanceMode !== AppearanceMode.SyncWithSystem) { @@ -10,3 +14,9 @@ export const getTheme = (appearanceMode: AppearanceMode) => { return theme; }; + +export const getThemeFromLocalStorage = () => + getTheme( + trySafe(() => z.nativeEnum(AppearanceMode).parse(localStorage.getItem(themeStorageKey))) ?? + AppearanceMode.SyncWithSystem + );