Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add dedicated error page #586

Merged
merged 5 commits into from Jan 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 25 additions & 25 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -42,7 +42,7 @@
"react-dom": "^17.0.2",
"react-i18next": "^12.0.0",
"react-router-bootstrap": "^0.26.2",
"react-router-dom": "^6.4.3"
"react-router-dom": "^6.6.1"
},
"scripts": {
"dev:start": "REACT_APP_JAM_DEV_MODE=true npm start",
Expand Down
106 changes: 62 additions & 44 deletions src/components/App.tsx
Expand Up @@ -14,8 +14,10 @@ import { useSessionConnectionError } from '../context/ServiceInfoContext'
import { useSettings } from '../context/SettingsContext'
import { useCurrentWallet, useSetCurrentWallet } from '../context/WalletContext'
import { clearSession, setSession } from '../session'
import { isDebugFeatureEnabled } from '../constants/debugFeatures'
import CreateWallet from './CreateWallet'
import Earn from './Earn'
import ErrorPage, { ErrorThrowingComponent } from './ErrorPage'
import Footer from './Footer'
import Jam from './Jam'
import Layout from './Layout'
Expand Down Expand Up @@ -55,58 +57,74 @@ export default function App() {
<>
<Navbar />
<rb.Container as="main" className="py-4 py-sm-5">
<Layout>
<Outlet />
</Layout>
<Outlet />
</rb.Container>
<Footer />
</>
}
errorElement={<ErrorPage />}
>
{/**
* This sections defines all routes that can be displayed, even if the connection
* to the backend is down, e.g. "create-wallet" shows the seed quiz and it is important
* that it stays visible in case the backend becomes unavailable.
*/}
<Route id="create-wallet" path={routes.createWallet} element={<CreateWallet startWallet={startWallet} />} />
<Route
id="error-boundary"
element={
<Layout>
<Outlet />
</Layout>
}
errorElement={
<Layout variant="wide">
<ErrorPage />
</Layout>
}
>
{/**
* This sections defines all routes that can be displayed, even if the connection
* to the backend is down, e.g. "create-wallet" shows the seed quiz and it is important
* that it stays visible in case the backend becomes unavailable.
*/}
<Route id="create-wallet" path={routes.createWallet} element={<CreateWallet startWallet={startWallet} />} />

{sessionConnectionError ? (
<Route
id="404"
path="*"
element={
<rb.Alert variant="danger">
{t('app.alert_no_connection', { connectionError: sessionConnectionError.message })}.
</rb.Alert>
}
/>
) : (
<>
{/**
* This section defines all routes that are displayed only if the backend is reachable.
*/}
{sessionConnectionError ? (
<Route
id="wallets"
path={routes.home}
element={<Wallets currentWallet={currentWallet} startWallet={startWallet} stopWallet={stopWallet} />}
id="404"
path="*"
element={
<rb.Alert variant="danger">
{t('app.alert_no_connection', { connectionError: sessionConnectionError.message })}.
</rb.Alert>
}
/>
{currentWallet && (
<>
<Route id="wallet" path={routes.wallet} element={<MainWalletView wallet={currentWallet} />} />
<Route id="jam" path={routes.jam} element={<Jam wallet={currentWallet} />} />
<Route id="send" path={routes.send} element={<Send wallet={currentWallet} />} />
<Route id="earn" path={routes.earn} element={<Earn wallet={currentWallet} />} />
<Route id="receive" path={routes.receive} element={<Receive wallet={currentWallet} />} />
<Route
id="settings"
path={routes.settings}
element={<Settings wallet={currentWallet} stopWallet={stopWallet} />}
/>
</>
)}
<Route id="404" path="*" element={<Navigate to={routes.home} replace={true} />} />
</>
)}
) : (
<>
{/**
* This section defines all routes that are displayed only if the backend is reachable.
*/}
<Route
id="wallets"
path={routes.home}
element={<Wallets currentWallet={currentWallet} startWallet={startWallet} stopWallet={stopWallet} />}
/>
{currentWallet && (
<>
<Route id="wallet" path={routes.wallet} element={<MainWalletView wallet={currentWallet} />} />
<Route id="jam" path={routes.jam} element={<Jam wallet={currentWallet} />} />
<Route id="send" path={routes.send} element={<Send wallet={currentWallet} />} />
<Route id="earn" path={routes.earn} element={<Earn wallet={currentWallet} />} />
<Route id="receive" path={routes.receive} element={<Receive wallet={currentWallet} />} />
<Route
id="settings"
path={routes.settings}
element={<Settings wallet={currentWallet} stopWallet={stopWallet} />}
/>
</>
)}
{isDebugFeatureEnabled('errorExamplePage') && (
<Route id="error-example" path={routes.__errorExample} element={<ErrorThrowingComponent />} />
)}
<Route id="404" path="*" element={<Navigate to={routes.home} replace={true} />} />
</>
)}
</Route>
</Route>
),
{
Expand Down
92 changes: 92 additions & 0 deletions src/components/ErrorPage.tsx
@@ -0,0 +1,92 @@
import { Trans, useTranslation } from 'react-i18next'
import * as rb from 'react-bootstrap'
import { useRouteError } from 'react-router-dom'
import PageTitle from './PageTitle'
import { t } from 'i18next'
import { useEffect } from 'react'

export function ErrorThrowingComponent() {
useEffect(() => {
throw new Error('This error is thrown on purpose. Only to be used for testing.')
}, [])
return <></>
}

interface ErrorViewProps {
title: string
subtitle: string
reason: string
stacktrace?: string
}

function ErrorView({ title, subtitle, reason, stacktrace }: ErrorViewProps) {
return (
<div>
<PageTitle title={title} subtitle={subtitle} />

<p>
<Trans i18nKey="error_page.report_bug">
Please{' '}
<a
href="https://github.com/joinmarket-webui/jam/issues/new?labels=bug&template=bug_report.md"
target="_blank"
rel="noopener noreferrer"
>
open an issue on GitHub
</a>{' '}
for this error to be reviewed and resolved in an upcoming version.
</Trans>
</p>

<div className="my-4">
<h6>{t('error_page.heading_reason')}</h6>
<rb.Alert variant="danger">{reason}</rb.Alert>
</div>

{stacktrace && (
<div className="my-4">
<h6>{t('error_page.heading_stacktrace')}</h6>
<pre className="border p-2">
<code>{stacktrace}</code>
</pre>
</div>
)}
</div>
)
}

function UnknownError({ error }: { error: any }) {
const { t } = useTranslation()

return (
<ErrorView
title={t('error_page.unknown_error.title')}
subtitle={t('error_page.unknown_error.subtitle')}
reason={error.message || t('global.errors.reason_unknown')}
stacktrace={error.stack}
/>
)
}

function ErrorWithDetails({ error }: { error: Error }) {
const { t } = useTranslation()

return (
<ErrorView
title={t('error_page.error_with_details.title')}
subtitle={t('error_page.error_with_details.subtitle')}
reason={error.message || t('global.errors.reason_unknown')}
stacktrace={error.stack}
/>
)
}

export default function ErrorPage() {
const error = useRouteError()

if (error instanceof Error) {
return <ErrorWithDetails error={error} />
} else {
return <UnknownError error={error} />
}
}
10 changes: 8 additions & 2 deletions src/components/PageTitle.jsx → src/components/PageTitle.tsx
@@ -1,7 +1,13 @@
import React from 'react'
import Sprite from './Sprite'

export default function PageTitle({ title, subtitle, success = false, center = false }) {
interface PageTitleProps {
title: string
subtitle?: string
success?: boolean
center?: boolean
}

export default function PageTitle({ title, subtitle, success = false, center = false }: PageTitleProps) {
return (
<div className={`mb-4 ${center && 'text-center'}`}>
{success && (
Expand Down
2 changes: 2 additions & 0 deletions src/constants/debugFeatures.ts
Expand Up @@ -2,6 +2,7 @@ interface DebugFeatures {
insecureScheduleTesting: boolean
allowCreatingExpiredFidelityBond: boolean
skipWalletBackupConfirmation: boolean
errorExamplePage: boolean
}

const devMode = process.env.NODE_ENV === 'development' && process.env.REACT_APP_JAM_DEV_MODE === 'true'
Expand All @@ -10,6 +11,7 @@ const debugFeatures: DebugFeatures = {
allowCreatingExpiredFidelityBond: devMode,
insecureScheduleTesting: devMode,
skipWalletBackupConfirmation: devMode,
errorExamplePage: devMode,
}

type DebugFeature = keyof DebugFeatures
Expand Down
1 change: 1 addition & 0 deletions src/constants/routes.ts
Expand Up @@ -8,4 +8,5 @@ export const routes = {
settings: '/settings',
wallet: '/wallet',
createWallet: '/create-wallet',
__errorExample: '/error-example',
}
13 changes: 13 additions & 0 deletions src/i18n/locales/en/translation.json
Expand Up @@ -73,6 +73,19 @@
"button_next": "Next",
"button_complete": "Let's go!"
},
"error_page": {
"unknown_error": {
"title": "An unknown error has been encountered",
"subtitle": "The source of the error could not be determined."
},
"error_with_details": {
"title": "Something broke :(",
"subtitle": ""
},
"heading_reason": "Reason:",
"heading_stacktrace": "Stacktrace:",
"report_bug": "Please <2>open an issue on GitHub</2> for this error to be reviewed and resolved in an upcoming version."
},
"wallets": {
"title": "Your Wallets",
"subtitle_no_wallets": "It looks like you do not have a wallet, yet.",
Expand Down