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
114 changes: 114 additions & 0 deletions Frontend/src/app/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use client'

/**
* Route-level error boundary (Next.js App Router).
*
* Catches errors thrown during client rendering of any page or layout beneath
* this file's directory. `global-error.tsx` handles the rare case where the
* root layout itself throws.
*
* Next.js contract:
* - Must be a Client Component ('use client').
* - Receives `error` (the thrown value, with `digest` for server-side logging)
* and `reset` (retries the failed segment).
* - The component is re-mounted when `reset` succeeds.
*/

import { useEffect } from 'react'

interface ErrorBoundaryProps {
error: Error & { digest?: string }
reset: () => void
}

/**
* Determine whether the thrown error originates from the Stellar wallet layer.
* We check `name` rather than `instanceof` because the error crosses the
* serialisation boundary between the server and the client error boundary.
*/
function isWalletError(error: Error): boolean {
return error.name === 'WalletConnectionError' || error.name === 'WalletNotConnectedError'
}

function isApiError(error: Error): boolean {
return error.name === 'ApiError'
}

export default function ErrorBoundary({ error, reset }: ErrorBoundaryProps) {
const walletError = isWalletError(error)
const apiError = isApiError(error)

useEffect(() => {
// Log to the browser console in development. In production you would
// forward `error.digest` to your observability service.
if (process.env.NODE_ENV !== 'production') {
console.error('[ErrorBoundary]', error)
}
}, [error])

return (
<div
role="alert"
aria-live="assertive"
className="error-boundary-container"
>
{/* Icon */}
<div className="error-boundary-icon" aria-hidden="true">
{walletError ? '🔗' : apiError ? '📡' : '⚠️'}
</div>

{/* Heading */}
<h1 className="error-boundary-heading">
{walletError
? 'Wallet connection problem'
: apiError
? 'We couldn\'t reach the server'
: 'Something went wrong'}
</h1>

{/* Human-readable description */}
<p className="error-boundary-description">
{walletError
? 'Vaulty couldn\'t connect to your Stellar wallet. Please check that your wallet extension is installed and unlocked, then try again.'
: apiError
? 'There was a problem communicating with the Vaulty server. Check your internet connection and try again.'
: 'An unexpected error occurred. Your funds are safe — this is only a display issue.'}
</p>

{/* Error reference — never expose the raw message or stack trace */}
{error.digest && (
<p className="error-boundary-reference">
Reference: <code>{error.digest}</code>
</p>
)}

{/* Actions */}
<div className="error-boundary-actions">
<button
onClick={reset}
className="error-boundary-retry-btn"
type="button"
>
Try again
</button>

<a href="/" className="error-boundary-home-link">
Go to home
</a>
</div>

{/* Wallet-specific guidance */}
{walletError && (
<aside className="error-boundary-wallet-tips" aria-label="Wallet troubleshooting tips">
<h2 className="error-boundary-tips-heading">Troubleshooting tips</h2>
<ul className="error-boundary-tips-list">
<li>Make sure your wallet browser extension is installed and enabled.</li>
<li>Unlock your wallet and grant permission when prompted.</li>
<li>Refresh the page and try connecting again.</li>
<li>If the issue persists, try a different browser or wallet.</li>
</ul>
</aside>
)}
</div>
)
}
190 changes: 190 additions & 0 deletions Frontend/src/app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
'use client'

/**
* Root-level error boundary (Next.js App Router).
*
* Catches errors thrown by the root layout itself — a much rarer case than a
* route error. Because the root layout has failed, this component must render
* a complete HTML document on its own; it cannot rely on the layout's <html>
* or <body> wrappers.
*
* Next.js contract:
* - Must be a Client Component ('use client').
* - Must render <html> and <body> itself.
* - Receives the same `error` / `reset` props as error.tsx.
*/

import { useEffect } from 'react'

interface GlobalErrorBoundaryProps {
error: Error & { digest?: string }
reset: () => void
}

export default function GlobalErrorBoundary({ error, reset }: GlobalErrorBoundaryProps) {
useEffect(() => {
if (process.env.NODE_ENV !== 'production') {
console.error('[GlobalErrorBoundary]', error)
}
}, [error])

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Vaulty — Something went wrong</title>
{/*
Inline critical styles so the page is readable even when the CSS
bundle has not loaded (which may itself be the cause of the error).
*/}
<style>{`
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

body {
font-family: system-ui, -apple-system, sans-serif;
background: #0c4a6e;
color: #f0f9ff;
min-height: 100dvh;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}

.gb-card {
background: #075985;
border: 1px solid #0369a1;
border-radius: 1rem;
max-width: 36rem;
width: 100%;
padding: 2.5rem 2rem;
text-align: center;
}

.gb-logo {
font-size: 2rem;
font-weight: 800;
letter-spacing: -0.05em;
margin-bottom: 1.5rem;
color: #38bdf8;
}

.gb-icon {
font-size: 3rem;
margin-bottom: 1rem;
display: block;
}

.gb-heading {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.75rem;
}

.gb-description {
font-size: 1rem;
line-height: 1.6;
color: #bae6fd;
margin-bottom: 1.5rem;
}

.gb-reference {
font-size: 0.75rem;
color: #7dd3fc;
margin-bottom: 1.5rem;
}

.gb-reference code {
background: #0c4a6e;
padding: 0.15em 0.4em;
border-radius: 0.25rem;
font-family: monospace;
}

.gb-actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
align-items: center;
}

@media (min-width: 400px) {
.gb-actions { flex-direction: row; justify-content: center; }
}

.gb-retry-btn {
background: #0284c7;
color: #fff;
border: none;
border-radius: 0.5rem;
padding: 0.6rem 1.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}

.gb-retry-btn:hover { background: #0369a1; }
.gb-retry-btn:focus-visible {
outline: 2px solid #38bdf8;
outline-offset: 2px;
}

.gb-home-link {
color: #7dd3fc;
font-size: 0.95rem;
text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer;
}

.gb-home-link:focus-visible {
outline: 2px solid #38bdf8;
outline-offset: 2px;
border-radius: 0.25rem;
}
`}</style>
</head>
<body>
<div
role="alert"
aria-live="assertive"
className="gb-card"
>
<div className="gb-logo" aria-label="Vaulty">🔐 Vaulty</div>

<span className="gb-icon" aria-hidden="true">💥</span>

<h1 className="gb-heading">Application error</h1>

<p className="gb-description">
Vaulty ran into a critical error and couldn&apos;t recover automatically.
Your funds are safe — this is a display problem only.
Please try reloading the page.
</p>

{error.digest && (
<p className="gb-reference">
Reference: <code>{error.digest}</code>
</p>
)}

<div className="gb-actions">
<button
onClick={reset}
className="gb-retry-btn"
type="button"
>
Reload app
</button>

<a href="/" className="gb-home-link">
Go to home
</a>
</div>
</div>
</body>
</html>
)
}
56 changes: 56 additions & 0 deletions Frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,60 @@
.input-field {
@apply w-full px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent;
}

/* ─── Error boundary — route-level (error.tsx) ──────────────────────────── */

.error-boundary-container {
@apply flex flex-col items-center justify-center min-h-[60vh] px-4 py-12 text-center;
}

.error-boundary-icon {
@apply text-5xl mb-4;
}

.error-boundary-heading {
@apply text-2xl font-bold text-slate-900 mb-3;
}

.error-boundary-description {
@apply text-slate-600 text-base leading-relaxed max-w-md mb-6;
}

.error-boundary-reference {
@apply text-xs text-slate-400 mb-6;
}

.error-boundary-reference code {
@apply bg-slate-100 px-1.5 py-0.5 rounded font-mono;
}

.error-boundary-actions {
@apply flex flex-col sm:flex-row gap-3 items-center mb-8;
}

.error-boundary-retry-btn {
@apply px-6 py-2.5 bg-primary-600 text-white rounded-lg font-semibold
hover:bg-primary-700 transition-colors
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2
focus-visible:outline-primary-500;
}

.error-boundary-home-link {
@apply text-primary-600 underline underline-offset-2 text-sm hover:text-primary-700
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2
focus-visible:outline-primary-500 rounded-sm;
}

/* Wallet-specific tip block */
.error-boundary-wallet-tips {
@apply bg-accent-50 border border-accent-200 rounded-lg p-5 max-w-md text-left;
}

.error-boundary-tips-heading {
@apply text-sm font-semibold text-accent-800 mb-2;
}

.error-boundary-tips-list {
@apply list-disc list-inside space-y-1 text-sm text-accent-700;
}
}
2 changes: 2 additions & 0 deletions Frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import { useState } from 'react';
import { useVault } from '@hooks/useVault';
import { useWallet } from '@hooks/useWallet';
Expand Down
Loading
Loading