Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 68 additions & 45 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Routes, Route, Navigate } from 'react-router-dom';
import { useEffect } from 'react';
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
import { useEffect, type ReactNode } from 'react';
import Home from './pages/Home';
import Debugger from './pages/Debugger';
import PayrollScheduler from './pages/PayrollScheduler';
Expand All @@ -25,6 +25,20 @@ import AuthCallback from './pages/AuthCallback';
import { useTranslation } from 'react-i18next';
import { contractService } from './services/contracts';

/**
* Wraps a route element in an ErrorBoundary that auto-resets when the user
* navigates to a different route path. This ensures a crash on one route
* doesn't permanently break navigation to another route.
*/
function RouteBoundary({ children, fallback }: { children: ReactNode; fallback?: ReactNode }) {
const location = useLocation();
return (
<ErrorBoundary key={location.pathname} fallback={fallback ?? <ErrorFallback />}>
{children}
</ErrorBoundary>
);
}

function App() {
const { t } = useTranslation();

Expand All @@ -41,7 +55,7 @@ function App() {
<Route
path="/"
element={
<ErrorBoundary
<RouteBoundary
fallback={
<ErrorFallback
title={t('errorFallback.homeTitle')}
Expand All @@ -50,13 +64,13 @@ function App() {
}
>
<Home />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/payroll"
element={
<ErrorBoundary
<RouteBoundary
fallback={
<ErrorFallback
title={t('errorFallback.payrollTitle')}
Expand All @@ -65,13 +79,13 @@ function App() {
}
>
<PayrollScheduler />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/employee"
element={
<ErrorBoundary
<RouteBoundary
fallback={
<ErrorFallback
title={t('errorFallback.employeesTitle')}
Expand All @@ -80,36 +94,31 @@ function App() {
}
>
<EmployeeEntry />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/portal"
element={
<ErrorBoundary
fallback={
<ErrorFallback
title="Employee Portal Error"
description="Something went wrong loading your portal."
/>
}
<RouteBoundary
fallback={<ErrorFallback title="Employee Portal Error" description="Something went wrong loading your portal." />}
>
<EmployeePortal />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/reports"
element={
<ErrorBoundary fallback={<ErrorFallback />}>
<RouteBoundary>
<CustomReportBuilder />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/debug"
element={
<ErrorBoundary
<RouteBoundary
fallback={
<ErrorFallback
title={t('errorFallback.debuggerTitle')}
Expand All @@ -118,13 +127,13 @@ function App() {
}
>
<Debugger />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/debug/:contractName"
element={
<ErrorBoundary
<RouteBoundary
fallback={
<ErrorFallback
title={t('errorFallback.debuggerTitle')}
Expand All @@ -133,95 +142,109 @@ function App() {
}
>
<Debugger />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/admin"
element={
<ErrorBoundary fallback={<ErrorFallback />}>
<RouteBoundary>
<AdminPanel />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/settings"
element={
<ErrorBoundary fallback={<ErrorFallback onReset={() => {}} />}>
<RouteBoundary>
<Settings />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/settings/webhooks"
element={
<ErrorBoundary fallback={<ErrorFallback onReset={() => {}} />}>
<RouteBoundary>
<WebhookSettings />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/help"
element={
<ErrorBoundary fallback={<ErrorFallback onReset={() => {}} />}>
<RouteBoundary>
<HelpCenter />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/cross-asset-payment"
element={
<ErrorBoundary fallback={<ErrorFallback onReset={() => {}} />}>
<RouteBoundary>
<CrossAssetPayment />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/transactions"
element={
<ErrorBoundary fallback={<ErrorFallback onReset={() => {}} />}>
<RouteBoundary>
<TransactionHistory />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/forecast"
element={
<ErrorBoundary fallback={<ErrorFallback onReset={() => {}} />}>
<RouteBoundary>
<Forecasting />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/vesting"
element={
<ErrorBoundary fallback={<ErrorFallback onReset={() => {}} />}>
<RouteBoundary>
<VestingEscrow />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/revenue-split"
element={
<ErrorBoundary fallback={<ErrorFallback onReset={() => {}} />}>
<RouteBoundary>
<RevenueSplitDashboard />
</ErrorBoundary>
</RouteBoundary>
}
/>
<Route
path="/tax-compliance"
element={
<ErrorBoundary fallback={<ErrorFallback onReset={() => {}} />}>
<RouteBoundary>
<TaxComplianceWizard />
</ErrorBoundary>
</RouteBoundary>
}
/>
</Route>
<Route path="/login" element={<Login />} />
<Route path="/auth-callback" element={<AuthCallback />} />
<Route
path="/login"
element={
<RouteBoundary>
<Login />
</RouteBoundary>
}
/>
<Route
path="/auth-callback"
element={
<RouteBoundary>
<AuthCallback />
</RouteBoundary>
}
/>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
);
}

export default App;
export default App;
57 changes: 55 additions & 2 deletions frontend/src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,28 @@ import * as Sentry from '@sentry/react';
type ErrorBoundaryProps = {
fallback: React.ReactNode;
children: React.ReactNode;
/** Callback invoked after the boundary resets (e.g. when the user clicks "Try Again"). */
onReset?: () => void;
/**
* When any of these values change between renders, the boundary is reset
* (and `onReset` is fired). Useful for keying the boundary to the current
* route so navigation away from a crashed route recovers automatically.
*/
resetKeys?: ReadonlyArray<unknown>;
};

type ErrorBoundaryState = {
hasError: boolean;
};

/**
* Error boundary that isolates render errors to the subtree it wraps. It
* renders `fallback` when a child throws, optionally injecting an `onReset`
* prop into the fallback so "Try Again" buttons can clear the error state.
*
* When `resetKeys` change between renders, the boundary recovers
* automatically (useful for route-based reset).
*/
export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = {
hasError: false,
Expand All @@ -19,19 +35,56 @@ export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, E
return { hasError: true };
}

componentDidUpdate(prevProps: ErrorBoundaryProps) {
const { resetKeys, onReset } = this.props;

// Auto-reset when resetKeys change (e.g. route navigation).
if (this.state.hasError && resetKeys && resetKeys.length > 0) {
const prevKeys = prevProps.resetKeys;
if (
!prevKeys ||
prevKeys.length !== resetKeys.length ||
resetKeys.some((key, index) => !Object.is(key, prevKeys[index]))
) {
this.setState({ hasError: false });
onReset?.();
}
}
}

componentDidCatch(error: unknown, errorInfo: React.ErrorInfo) {
// Log to Sentry when configured, and always surface the component stack
// in the console for dev-mode debugging.
console.error('Uncaught error caught by ErrorBoundary', error, errorInfo);
Sentry.captureException(error, {
extra: {
componentStack: errorInfo.componentStack,
},
});
}

handleReset = () => {
this.setState({ hasError: false });
this.props.onReset?.();
};

render() {
if (this.state.hasError) {
return this.props.fallback;
const { fallback } = this.props;
// If the fallback accepts an onReset handler, wire it up so the
// "Try Again" button actually clears the error state.
if (React.isValidElement(fallback)) {
const fallbackProps = fallback.props as { onReset?: unknown };
if (typeof fallbackProps.onReset === 'undefined') {
return React.cloneElement(
fallback as React.ReactElement<{ onReset?: () => void }>,
{ onReset: this.handleReset },
);
}
}
return fallback;
}

return this.props.children;
}
}
}
2 changes: 1 addition & 1 deletion frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<SocketProvider>
<WalletProvider>
<BrowserRouter>
<ErrorBoundary fallback={<ErrorFallback onReset={() => {}} />}>
<ErrorBoundary fallback={<ErrorFallback />}>
<App />
</ErrorBoundary>
</BrowserRouter>
Expand Down