diff --git a/ERROR_PAGES_README.md b/ERROR_PAGES_README.md index 9d966d6e..08474626 100644 --- a/ERROR_PAGES_README.md +++ b/ERROR_PAGES_README.md @@ -34,170 +34,219 @@ This project includes comprehensive, user-friendly error pages for various error - "Go Home" button - "Report Issue" link -**How it's triggered**: -- Next.js automatically catches errors and displays this page -- Wrap components with error boundaries as needed +# Error Pages Recovery Flows ---- +This document is the source of truth for CommitLabs error surfaces in the App Router. It covers the visible UI, the user recovery path, and the behavior that the code currently implements so the docs can be checked against the actual components. -### 3. **Network Error Page** (`/network-error`) -- **File**: `src/app/network-error/page.tsx` -- **Styling**: `src/app/network-error/page.module.css` -- **Purpose**: Manual route for network connectivity issues -- **Features**: - - Connection error message - - Network troubleshooting checklist: - - Check internet connection - - Disable VPN/proxy - - Restart router - - Check other websites - - Clear cache and cookies - - Live connection status indicator with animated dot - - "Retry" button (checks connectivity) - - "Go Home" button +## Overview -**How to use it**: -```tsx -// Navigate users here when network errors occur -import { useRouter } from 'next/navigation' +| Surface | File | Route | Recovery focus | +| --- | --- | --- | --- | +| Generic app error boundary | `src/app/error.tsx` | App-level `error.tsx` boundary | Retry the segment, return home, report the issue | +| Not found page | `src/app/not-found.tsx` | App-level `not-found.tsx` boundary | Return home or go back | +| Network error page | `src/app/network-error/page.tsx` | `/network-error` | Probe connectivity, retry, or return home | +| Transaction error page | `src/app/transaction-error/page.tsx` | `/transaction-error` | Review state, retry, inspect explorer, or go to dashboard | -const router = useRouter() +All four surfaces use the shared `ErrorLayout` and `ErrorButton` primitives from `src/components/` so the spacing, button styling, and overall framing stay consistent. -try { - await fetchData() -} catch (error) { - router.push('/network-error') -} -``` +## 1. `error.tsx` Generic Error Page ---- +### Purpose and triggers -### 4. **Transaction Error Page** (`/transaction-error`) -- **File**: `src/app/transaction-error/page.tsx` -- **Styling**: `src/app/transaction-error/page.module.css` -- **Purpose**: Display blockchain/transaction-specific errors -- **Features**: - - Custom error messages (via URL params) - - Transaction hash display with copy button - - Error code display - - Troubleshooting tips: - - Insufficient balance - - Network congestion - - Invalid parameters - - Wallet/signature issues - - Contract execution failures - - "Try Again" button - - "Go Home" button - - "View on Explorer" link (links to Stellar Expert) - - "Contact Support" link +`src/app/error.tsx` is the app-level error boundary UI. Next.js shows it when a server or client render error escapes the current route segment. In development, the Next.js overlay can still take precedence for compile-time/runtime debugging, but this file is the production recovery surface. -**How to use it**: -```tsx -// Navigate with transaction details -router.push( - `/transaction-error?message=Transaction failed&hash=abc123def456&code=INSUFFICIENT_BALANCE` -) +### UI copy and layout -// URL Parameters: -// - message: Error message to display -// - hash: Transaction hash (optional) -// - code: Error code (optional) +- Large `500` code block +- Title: `Something Went Wrong` +- Body copy: `We're experiencing technical difficulties. Our team has been notified and is working to fix the issue.` +- Optional error details area that shows `error.message` and `error.digest` +- Three actions in order: `Try Again`, `Go Home`, `Report Issue` + +Pseudo-screenshot: + +```text +┌──────────────────────────────────┐ +│ 500 │ +│ Something Went Wrong │ +│ We're experiencing technical... │ +│ Error ID: digest-if-present │ +│ [Try Again] [Go Home] [Report...] │ +└──────────────────────────────────┘ ``` ---- +### Recovery flow -## Shared Components +- `Try Again` calls the App Router `reset()` callback so the segment can re-render. +- `Go Home` sends the user to `/`. +- `Report Issue` opens the Stellar contact page in a new tab. -### ErrorLayout (`src/components/ErrorLayout.tsx`) -Wrapper component that provides consistent page structure and styling for all error pages. +### Behavior notes -```tsx -import ErrorLayout from '@/components/ErrorLayout' - -export default function MyErrorPage() { - return ( - - {/* Your error content */} - - ) -} -``` +- If `error.digest` exists, it is surfaced as an error ID for support triage. +- The page is intentionally generic and does not attempt to infer a route-specific recovery path. -### ErrorButton (`src/components/ErrorButton.tsx`) -Reusable button component for error pages with multiple variants. +## 2. `not-found.tsx` 404 Page -```tsx -import ErrorButton from '@/components/ErrorButton' +### When it is triggered -// As a link -Go Home +`src/app/not-found.tsx` renders when Next.js resolves a route to 404. It is the fallback for nonexistent pages and moved content. -// As a secondary button -Back +### UI copy and behavior -// As a button with click handler -Try Again +- Large `404` code block +- Title: `Page Not Found` +- Body copy: `The page you're looking for doesn't exist or has been moved.` +- Search input with placeholder `Search the site...` +- Actions: `Go Home` and `Go Back` -// As an external link - - External Link - +Pseudo-screenshot: -// Disabled state -Disabled Button +```text +┌─────────────────────────────────┐ +│ 404 │ +│ Page Not Found │ +│ The page you're looking for... │ +│ [Search the site...] │ +│ [Go Home] [Go Back] │ +└─────────────────────────────────┘ ``` ---- +### Recovery navigation + +- `Go Home` routes to `/`. +- `Go Back` calls `router.back()`. +- The search input currently logs the query on Enter; it does not submit to a real search endpoint yet. + +## 3. `network-error` Network Error Page + +### When it displays + +`src/app/network-error/page.tsx` is a manual recovery page, not an automatic Next.js boundary. The app can navigate here when a fetch or connectivity check fails and the UX wants to give the user an explicit retry path. + +### UI copy and layout + +- Large connection-themed icon +- Title: `Connection Error` +- Body copy: `Unable to connect to the network. Please check your internet connection and try again.` +- Troubleshooting checklist: + - Check internet access + - Disable VPN or proxy + - Restart the router or mobile connection + - Check whether other sites load + - Clear browser cache and cookies +- Status line that switches between `No internet connection detected` and `Checking connection...` +- Actions: `Retry` and `Go Home` + +Pseudo-screenshot: + +```text +┌────────────────────────────────────┐ +│ Connection Error │ +│ Unable to connect to the network. │ +│ What you can do: │ +│ • Check your connection │ +│ • Disable VPN/proxy │ +│ ... │ +│ Status: Checking connection... │ +│ [Retry] [Go Home] │ +└────────────────────────────────────┘ +``` -## Design System +### Retry mechanism and countdown behavior -### Colors -- **Background**: Purple gradient (`#667eea` to `#764ba2`) -- **Text**: White -- **Primary Button**: White with purple text -- **Secondary Button**: Transparent white with border +- `Retry` performs a `HEAD` request against `/`. +- While the check is in flight, the button label changes to `Retrying...` and the status line changes to `Checking connection...`. +- If the probe succeeds, the page reloads. +- If the probe fails, the retry state is cleared and the page stays put. +- There is no countdown timer in the current implementation; the recovery feedback is the live retry state text. -### Typography -- **Error Code**: 6rem, 900 weight (4rem on mobile) -- **Title**: 2.5rem, 700 weight (2rem on mobile) -- **Description**: 1.1rem, 0.85 opacity (1rem on mobile) +### Recovery CTAs -### Spacing -- Container max-width: 600px -- Padding: 2rem (1.5rem on mobile) -- Gap between elements: 1-2rem +- `Retry` is the primary recovery action. +- `Go Home` sends the user back to `/` if they prefer to exit the recovery surface. -### Responsive Design -- Full-width padding on mobile -- Stacked buttons on screens < 640px -- Touch-friendly tap targets (min 44px) -- Adjusted font sizes for mobile readability +## 4. `transaction-error` Transaction Error Handler ---- +### Route-level behavior -## Animations +`src/app/transaction-error/page.tsx` is a sharable route for transaction-specific recovery. It reads `category`, `code`, `message`, and `hash` from the query string and then chooses the appropriate recovery copy. -All error pages include smooth entrance animations: -```css -@keyframes slideIn { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} +### Category selection rules + +- `category=rejected` renders the rejection flow. +- `category=timed-out` renders the timeout flow. +- `category=failed` renders the failure flow. +- If `category` is missing, the page falls back to `code` mapping, then to `failed`. + +### Route-level recovery copy and CTAs + +| Category | Title | Primary recovery | Secondary recovery | +| --- | --- | --- | --- | +| `rejected` | `Transaction Rejected` | `Try Again` | `Go to Dashboard` | +| `timed-out` | `Transaction Status Unknown` | `Try Again` plus explorer check when a hash exists | `Go to Dashboard` | +| `failed` | `Transaction Failed` | `Try Again` | `Go to Dashboard` | + +When a hash is present: + +- `timed-out` shows `Check Explorer`. +- `failed` and `rejected` show `View on Explorer` when an explorer URL can be built. +- The page also shows a copy button for the transaction hash. + +Pseudo-screenshot: + +```text +┌────────────────────────────────────────────┐ +│ Rejected / Timed out / Failed │ +│ Title and recovery summary │ +│ Transaction Hash: ... [Copy] │ +│ Error Code: ... │ +│ [Try Again] [Go to Dashboard] [Explorer] │ +└────────────────────────────────────────────┘ ``` -Status indicators (like network connection) have pulsing animations for visual feedback. +### Error category to CTA mapping used by the transaction progress modal ---- +The transaction flow also uses `src/app/TransactionProgressModal.tsx` for in-progress transaction outcomes. This is the richer category-to-CTA map that should stay in sync with the docs. -## Using Error Pages in Your Application +| Error code | Lead copy | Primary CTA | Secondary CTA | Recovery behavior | +| --- | --- | --- | --- | --- | +| `USER_REJECTED` | `Signature Canceled` | `Try Again` | `Close` | Calls the retry handler; no funds are moved | +| `INSUFFICIENT_BALANCE` | `Insufficient Balance` | `Fund Wallet` | `Close` | Currently closes the modal; intended for wallet-funding recovery | +| `NETWORK_CONGESTION` | `Network is Busy` | `Try Again` | `Close` | Calls the retry handler after a short wait | +| `RPC_TIMEOUT` | `Status Unknown (Timeout)` | `Check Explorer ↗` | `Close` | Opens the explorer when a hash exists, then avoids duplicate submission | +| `SLIPPAGE_EXCEEDED` | `Price Changed` | `Update Price` | `Cancel` | Returns to the price-edit flow | +| `CONTRACT_REVERTED` | `Contract Execution Failed` | `View Details` | `Close` | Surfaces the failure context for review | +| `UNKNOWN_ERROR` | `Unexpected Error` | `Try Again` | `Contact Support` | Default fallback when no specific mapping exists | + +### UX flow by category + +- Rejected errors ask the user to confirm the wallet prompt or review the changed parameters before retrying. +- Timeout errors bias toward explorer inspection first, because the transaction may still settle on-chain. +- Execution failures keep the retry path available, but the copy encourages checking the chain error first. +- Wallet and support-oriented actions are explicit in the modal, while the route-level page keeps the dashboard and explorer paths visible. + +## Shared components + +### `ErrorLayout` (`src/components/ErrorLayout.tsx`) + +Provides the outer container that centers error content and keeps the error pages visually consistent. + +### `ErrorButton` (`src/components/ErrorButton.tsx`) + +Renders an internal link, external link, or button depending on props. The error pages use it for all recovery CTAs so labels stay easy to audit. + +## Environment-specific behavior + +- Development can show the Next.js overlay before `error.tsx` for build and runtime diagnostics. +- Production uses the documented error surfaces directly. +- The network error page is manual and only appears when the app navigates there. +- The transaction error route is query-string driven and is safe to link to from async flows or support docs. + +## Example usage + +### Navigate to the network error page -### 1. Redirect to Network Error ```tsx import { useRouter } from 'next/navigation' @@ -208,7 +257,7 @@ export default function MyComponent() { try { const response = await fetch('/api/data') if (!response.ok) throw new Error('Failed to fetch') - } catch (error) { + } catch { router.push('/network-error') } } @@ -217,68 +266,17 @@ export default function MyComponent() { } ``` -### 2. Redirect to Transaction Error -```tsx -const handleTransaction = async () => { - try { - const result = await submitTransaction(data) - } catch (error) { - const message = error.message || 'Transaction failed' - const hash = error.txHash || '' - const code = error.code || '' - - router.push( - `/transaction-error?message=${encodeURIComponent(message)}&hash=${hash}&code=${code}` - ) - } -} -``` - -### 3. Auto-catch Errors in Layout -The `error.tsx` file at the app level automatically catches and displays errors from any route. - ---- +### Navigate to the transaction error page -## Browser Support - -- Modern browsers (Chrome, Firefox, Safari, Edge) -- Responsive design works on mobile, tablet, and desktop -- CSS features used: Flexbox, Grid, CSS animations, gradients - ---- - -## Customization - -### Change Colors -Edit `src/app/globals.css`: -```css -body { - background: linear-gradient(135deg, YOUR_COLOR_1 0%, YOUR_COLOR_2 100%); -} +```tsx +router.push( + `/transaction-error?message=${encodeURIComponent('Transaction failed')}&hash=${txHash}&code=${errorCode}`, +) ``` -### Change Error Messages -Edit the `title` and `description` text in each error page component. +## Drift-guard expectation -### Add More Error Types -Create a new page directory (e.g., `/rate-limit-error/page.tsx`) and follow the same pattern as the existing error pages. - ---- - -## Testing Error Pages Locally - -```bash -# Build and start the production server -npm run build -npm start - -# Then navigate to: -# - http://localhost:3000/404 (non-existent page) -# - http://localhost:3000/network-error -# - http://localhost:3000/transaction-error?message=Test&hash=abc123 -``` - ---- +If a new error surface is added under `src/app`, update this document at the same time. The drift-guard test compares the documented surfaces against the actual route files so that new error pages do not silently bypass review. ## File Structure diff --git a/README.md b/README.md index cc9f0aa5..09ba7b31 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ The frontend application for the CommitLabs protocol, a decentralized platform f - [Project Structure](#project-structure) - [Backend API Changelog](#backend-api-changelog) - [Settlement and Early Exit UI Flows](docs/settlement-and-early-exit-flows.md) +- [Error Page Recovery Flows](ERROR_PAGES_README.md) - [Contributing](#contributing) - [API Reference](#api-reference) - [License](#license) @@ -33,6 +34,7 @@ This frontend interacts with the CommitLabs Soroban smart contracts to: - **Marketplace**: Browse and filter active commitments available for purchase. - **Wallet Integration**: Connect with Stellar wallets (e.g., Freighter) to sign transactions. - **Settlement and Early Exit Flows**: Guided settlement eligibility, settlement success, and early-exit confirmation surfaces backed by preview and execution endpoints. See [Settlement and Early Exit UI Flows](docs/settlement-and-early-exit-flows.md). +- **Error Page Recovery Flows**: App Router error boundaries and recovery pages are documented in [ERROR_PAGES_README.md](ERROR_PAGES_README.md). - **Responsive Design**: Optimized for both desktop and mobile devices. ## 🏗 Architecture diff --git a/src/app/__tests__/errorPagesDoc.test.ts b/src/app/__tests__/errorPagesDoc.test.ts new file mode 100644 index 00000000..41c589e5 --- /dev/null +++ b/src/app/__tests__/errorPagesDoc.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, it } from 'vitest' +import { existsSync, readFileSync, readdirSync } from 'node:fs' +import { basename, dirname, join, relative, resolve } from 'node:path' + +type ErrorSurface = { + filePath: string + routePath: string +} + +const ROOT = resolve(process.cwd()) +const APP_DIR = join(ROOT, 'src/app') +const DOC_PATH = join(ROOT, 'ERROR_PAGES_README.md') +const README_PATH = join(ROOT, 'README.md') + +const DOCUMENTED_SURFACES = [ + { filePath: 'src/app/error.tsx', routePath: '/error' }, + { filePath: 'src/app/not-found.tsx', routePath: '/not-found' }, + { filePath: 'src/app/network-error/page.tsx', routePath: '/network-error' }, + { filePath: 'src/app/transaction-error/page.tsx', routePath: '/transaction-error' }, +] as const + +const TRANSACTION_MODAL_PATH = 'src/app/TransactionProgressModal.tsx' + +function readWorkspaceFile(filePath: string): string { + return readFileSync(join(ROOT, filePath), 'utf8') +} + +function normalizeText(value: string): string { + return value.replace(/\s+/g, ' ').trim() +} + +function discoverErrorSurfaces(dirPath: string): ErrorSurface[] { + const surfaces: ErrorSurface[] = [] + + for (const entry of readdirSync(dirPath, { withFileTypes: true })) { + const entryPath = join(dirPath, entry.name) + + if (entry.isDirectory()) { + surfaces.push(...discoverErrorSurfaces(entryPath)) + continue + } + + const relativePath = relative(ROOT, entryPath).replace(/\\/g, '/') + + if (relativePath === 'src/app/error.tsx') { + surfaces.push({ filePath: relativePath, routePath: '/error' }) + continue + } + + if (relativePath === 'src/app/not-found.tsx') { + surfaces.push({ filePath: relativePath, routePath: '/not-found' }) + continue + } + + if (relativePath.endsWith('/page.tsx')) { + const routeFolder = basename(dirname(entryPath)) + + if (routeFolder.includes('error')) { + surfaces.push({ filePath: relativePath, routePath: `/${routeFolder}` }) + } + } + } + + return surfaces +} + +function expectSourceContainsAll(sourceText: string, labels: string[]) { + for (const label of labels) { + expect(sourceText).toContain(label) + } +} + +describe('error pages documentation drift guard', () => { + it('keeps the documented error-page files present at the expected paths', () => { + for (const surface of DOCUMENTED_SURFACES) { + expect(existsSync(join(ROOT, surface.filePath))).toBe(true) + } + + expect(existsSync(join(ROOT, TRANSACTION_MODAL_PATH))).toBe(true) + }) + + it('documents each known error surface and links the main README', () => { + const docText = readWorkspaceFile('ERROR_PAGES_README.md') + const readmeText = readWorkspaceFile('README.md') + + expect(readmeText).toContain('[Error Page Recovery Flows](ERROR_PAGES_README.md)') + expect(normalizeText(readmeText)).toContain('Error Page Recovery Flows') + + for (const surface of DOCUMENTED_SURFACES) { + expect(docText).toContain(surface.filePath) + expect(docText).toContain(surface.routePath) + } + + expect(docText).toContain('src/app/TransactionProgressModal.tsx') + expect(docText).toContain('There is no countdown timer in the current implementation') + }) + + it('keeps the documented recovery CTA labels in sync with the actual components', () => { + const errorPageText = readWorkspaceFile('src/app/error.tsx') + const notFoundText = readWorkspaceFile('src/app/not-found.tsx') + const networkErrorText = readWorkspaceFile('src/app/network-error/page.tsx') + const transactionErrorText = readWorkspaceFile('src/app/transaction-error/page.tsx') + const transactionModalText = readWorkspaceFile(TRANSACTION_MODAL_PATH) + + expectSourceContainsAll(errorPageText, ['Try Again', 'Go Home', 'Report Issue']) + expectSourceContainsAll(notFoundText, ['Go Home', 'Go Back']) + expectSourceContainsAll(networkErrorText, ['Retry', 'Go Home', 'Checking connection...', 'Retrying...']) + expectSourceContainsAll(transactionErrorText, ['Try Again', 'Go to Dashboard', 'Check Explorer', 'View on Explorer']) + expectSourceContainsAll(transactionModalText, [ + 'USER_REJECTED', + 'INSUFFICIENT_BALANCE', + 'NETWORK_CONGESTION', + 'RPC_TIMEOUT', + 'SLIPPAGE_EXCEEDED', + 'CONTRACT_REVERTED', + 'UNKNOWN_ERROR', + 'Try Again', + 'Fund Wallet', + 'Check Explorer ↗', + 'Update Price', + 'View Details', + 'Contact Support', + ]) + }) + + it('flags new undocumented error routes under src/app', () => { + const discoveredSurfaces = discoverErrorSurfaces(APP_DIR) + const documentedFilePaths = new Set(DOCUMENTED_SURFACES.map((surface) => surface.filePath)) + const documentedRoutePaths = new Set(DOCUMENTED_SURFACES.map((surface) => surface.routePath)) + + expect(discoveredSurfaces.map((surface) => surface.filePath).sort()).toEqual( + [...documentedFilePaths].sort(), + ) + + for (const surface of discoveredSurfaces) { + expect(documentedRoutePaths.has(surface.routePath)).toBe(true) + } + }) + + it('covers the helper branches used for route discovery and file validation', () => { + expect(existsSync(DOC_PATH)).toBe(true) + expect(existsSync(README_PATH)).toBe(true) + expect(normalizeText(readWorkspaceFile('ERROR_PAGES_README.md'))).toContain('Error Pages Recovery Flows') + }) +}) \ No newline at end of file