diff --git a/public/locales/en.json b/public/locales/en.json
index c2d9102..cee7495 100644
--- a/public/locales/en.json
+++ b/public/locales/en.json
@@ -84,6 +84,12 @@
},
"error": "Error",
+ "errorBoundary": {
+ "title": "Critical error",
+ "description": "A critical error has occurred in KristWeb, so this page was terminated. See console for details.",
+ "sentryNote": "This error was automatically reported."
+ },
+
"loading": "Loading...",
"copy": "Copy to clipboard",
diff --git a/src/App.tsx b/src/App.tsx
index 7935a67..30640c7 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -13,6 +13,7 @@
// FIXME: Apparently the import order of my CSS is important. Who knew!
import "./App.less";
+import { ErrorBoundary } from "@global/ErrorBoundary";
import { AppLoading } from "@global/AppLoading";
import { AppServices } from "@global/AppServices";
import { WebsocketProvider } from "@global/ws/WebsocketProvider";
@@ -34,22 +35,24 @@
store = initStore();
}
- return }>
-
-
-
-
-
-
+ return
+ }>
+
+
+
+
+
+
- {/* Services, etc. */}
-
-
-
-
-
-
- ;
+ {/* Services, etc. */}
+
+
+
+
+
+
+
+ ;
}
export default App;
diff --git a/src/components/results/SmallResult.tsx b/src/components/results/SmallResult.tsx
index 5e2f80f..acd4b16 100644
--- a/src/components/results/SmallResult.tsx
+++ b/src/components/results/SmallResult.tsx
@@ -5,7 +5,7 @@
// This is ant-design's Result component, but without importing 54 kB of images
// that we don't even use.
//
-// This file is based off of hte following source code from ant-design, which is
+// This file is based off of the following source code from ant-design, which is
// licensed under the MIT license:
//
// https://github.com/ant-design/ant-design/blob/077443696ba0fb708f2af81f5eb665b908d8be66/components/result/index.tsx
diff --git a/src/global/AppRouter.tsx b/src/global/AppRouter.tsx
index 396b627..c6a840f 100644
--- a/src/global/AppRouter.tsx
+++ b/src/global/AppRouter.tsx
@@ -1,9 +1,8 @@
// Copyright (c) 2020-2021 Drew Lemmy
// This file is part of KristWeb 2 under AGPL-3.0.
// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt
-import { Alert } from "antd";
-
import { Switch, Route, Redirect } from "react-router-dom";
+import { ErrorBoundary } from "@global/ErrorBoundary";
import { DashboardPage } from "@pages/dashboard/DashboardPage";
import { WalletsPage } from "@pages/wallets/WalletsPage";
@@ -100,9 +99,9 @@
component && (
{/* Try to catch errors on a route without crashing everything */}
-
+
{component}
-
+
)
))}
diff --git a/src/global/ErrorBoundary.tsx b/src/global/ErrorBoundary.tsx
new file mode 100644
index 0000000..66f079c
--- /dev/null
+++ b/src/global/ErrorBoundary.tsx
@@ -0,0 +1,44 @@
+// Copyright (c) 2020-2021 Drew Lemmy
+// This file is part of KristWeb 2 under AGPL-3.0.
+// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt
+import { FC } from "react";
+import { Alert } from "antd";
+
+import { useTFns } from "@utils/i18n";
+
+import * as Sentry from "@sentry/react";
+
+interface Props {
+ name: string;
+}
+
+export const ErrorBoundary: FC = ({ name, children }) => {
+ return }
+ onError={console.error}
+
+ // Add the boundary name to the scope
+ beforeCapture={scope => {
+ scope.setTag("error-boundary", name);
+ }}
+ >
+ {children}
+ ;
+};
+
+function ErrorFallback(): JSX.Element {
+ const { tStr } = useTFns("errorBoundary.");
+
+ return
+ {tStr("description")}
+
+ {/* TODO: Hide this if Sentry is disabled */}
+ {tStr("sentryNote")}
+ >}
+ />;
+}
diff --git a/src/pages/dev/DevPage.tsx b/src/pages/dev/DevPage.tsx
index 5ae7424..bfaa040 100644
--- a/src/pages/dev/DevPage.tsx
+++ b/src/pages/dev/DevPage.tsx
@@ -1,7 +1,8 @@
// Copyright (c) 2020-2021 Drew Lemmy
// This file is part of KristWeb 2 under AGPL-3.0.
// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt
-import { Button } from "antd";
+import { useState } from "react";
+import { Button, Space } from "antd";
import { PageLayout } from "@layout/PageLayout";
import { useWallets, deleteWallet } from "@wallets";
@@ -12,34 +13,39 @@
export function DevPage(): JSX.Element {
const { wallets } = useWallets();
+ const [forceError, setForceError] = useState(false);
+ if (forceError) throw new Error("Whoops!");
+
return
- {/* Delete all wallets with zero balance */}
-
+ {/* Delete all wallets */}
+ Object.values(wallets).forEach(deleteWallet)}>
+ Delete all wallets
+
-
+ {/* Clear local storage */}
+ { localStorage.clear(); location.reload(); }}>
+ Clear local storage
+
- {/* Delete all wallets */}
- Object.values(wallets).forEach(deleteWallet)}>
- Delete all wallets
-
-
-
-
- {/* Clear local storage */}
- { localStorage.clear(); location.reload(); }}>
- Clear local storage
-
+ {/* Cause an error */}
+ setForceError(true)}>
+ Cause an error
+
+
;
}