Skip to content

Commit

Permalink
feat: add dedicated error page (#586)
Browse files Browse the repository at this point in the history
* feat(wip): add basic error page skeleton

* refactor: PageTitle component from jsx to tsx

* feat: add basic error page

* test: ability to render example error page in dev mode

* Apply suggestions from code review

Co-authored-by: Gigi <109058+dergigi@users.noreply.github.com>
  • Loading branch information
theborakompanioni and dergigi committed Jan 23, 2023
1 parent 4c7ba2b commit 42c9f5d
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 72 deletions.
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

0 comments on commit 42c9f5d

Please sign in to comment.