diff --git a/dashboard/WEBHOOK_NOTIFICATIONS.md b/dashboard/WEBHOOK_NOTIFICATIONS.md new file mode 100644 index 0000000..c6ca8a3 --- /dev/null +++ b/dashboard/WEBHOOK_NOTIFICATIONS.md @@ -0,0 +1,314 @@ +# Webhook Event Notifications UI + +## Overview + +This document describes the webhook event notifications feature implemented in the AnchorPoint dashboard. This feature allows users to view and manage webhook events and transaction notifications through a comprehensive UI. + +## Components + +### 1. NotificationBell + +A header component that displays a notification bell icon with an unread count badge. + +**Features:** +- Real-time unread notification count +- Dropdown preview of recent notifications (last 5) +- Auto-polling every 30 seconds for new notifications +- Click-outside to close dropdown +- "View All" button to navigate to full notification center + +**Props:** +- `apiBaseUrl` (optional): API base URL, defaults to `http://localhost:3002` +- `onViewAll` (optional): Callback when "View All" is clicked + +**Usage:** +```tsx +import { NotificationBell } from './components/NotificationBell'; + + setActiveTab('notifications')} +/> +``` + +### 2. NotificationCenter + +A full-page notification management interface. + +**Features:** +- Statistics dashboard (Total, Sent, Pending, Failed) +- Filter notifications by status (All, PENDING, SENT, FAILED) +- Refresh button to manually fetch latest notifications +- Link to notification preferences +- Detailed notification list with: + - Status icons and badges + - Message content + - Notification type (EMAIL, SMS, PUSH) + - Transaction ID (if applicable) + - Relative timestamps + +**Props:** +- `apiBaseUrl` (optional): API base URL +- `onOpenPreferences` (optional): Callback to open preferences panel + +**Usage:** +```tsx +import NotificationCenter from './components/NotificationCenter'; + + setActiveTab('notification-preferences')} +/> +``` + +### 3. NotificationPreferences + +A settings panel for managing notification preferences. + +**Features:** +- Toggle email notifications +- Toggle SMS notifications (with phone number input) +- Toggle push notifications +- Save preferences with success/error feedback +- Information card explaining webhook notifications + +**Props:** +- `apiBaseUrl` (optional): API base URL + +**Usage:** +```tsx +import NotificationPreferences from './components/NotificationPreferences'; + + +``` + +## API Integration + +### Endpoints Used + +1. **GET /api/notifications/history** + - Fetches notification history (last 50 notifications) + - Requires authentication token + - Returns array of notification objects + +2. **GET /api/notifications/preferences** + - Fetches user's notification preferences + - Returns: `{ emailEnabled, smsEnabled, pushEnabled, phone }` + +3. **PATCH /api/notifications/preferences** + - Updates user's notification preferences + - Body: `{ emailEnabled?, smsEnabled?, pushEnabled?, phone? }` + +### Authentication + +All API requests require an authentication token stored in `localStorage` under the key `authToken`: + +```typescript +const token = localStorage.getItem('authToken'); +const response = await fetch(url, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, +}); +``` + +## Data Models + +### Notification Object + +```typescript +interface Notification { + id: string; + userId: string; + transactionId: string | null; + type: 'EMAIL' | 'SMS' | 'PUSH'; + status: 'PENDING' | 'SENT' | 'FAILED'; + message: string; + createdAt: string; // ISO 8601 timestamp +} +``` + +### Notification Preferences + +```typescript +interface Preferences { + emailEnabled: boolean; + smsEnabled: boolean; + pushEnabled: boolean; + phone?: string; +} +``` + +## Webhook Events + +The system supports the following webhook events: + +1. **transaction.status_changed** + - Triggered when a transaction status changes + - Includes transaction details and previous status + - Delivered via configured notification channels + +2. **KYC updates** + - Verification status changes + - Document approval/rejection + +3. **Multisig transaction events** + - New signature requests + - Transaction approvals + - Threshold reached notifications + +## Styling + +The components use Tailwind CSS with a dark theme consistent with the AnchorPoint dashboard: + +- **Primary color**: Configurable via CSS variables (`--primary`) +- **Background**: Dark slate tones (`bg-slate-900`, `bg-slate-800`) +- **Borders**: Subtle slate borders (`border-slate-700`) +- **Text**: Light slate for readability (`text-slate-100`, `text-slate-400`) + +### Status Colors + +- **SENT**: Emerald (`text-emerald-400`, `bg-emerald-500/10`) +- **FAILED**: Red (`text-red-400`, `bg-red-500/10`) +- **PENDING**: Amber (`text-amber-400`, `bg-amber-500/10`) + +## Accessibility + +All components follow accessibility best practices: + +- Semantic HTML elements +- ARIA labels and roles +- Keyboard navigation support +- Focus indicators +- Screen reader friendly +- Color contrast compliance + +## Testing + +### Manual QA Steps + +1. **NotificationBell** + - [ ] Bell icon displays in header + - [ ] Unread count badge shows correct number + - [ ] Clicking bell opens dropdown + - [ ] Dropdown shows last 5 notifications + - [ ] Clicking outside closes dropdown + - [ ] "View All" navigates to notification center + - [ ] Auto-polling updates notifications every 30s + +2. **NotificationCenter** + - [ ] Statistics cards show correct counts + - [ ] Filter buttons work (All, PENDING, SENT, FAILED) + - [ ] Refresh button fetches latest notifications + - [ ] Notifications display with correct status icons + - [ ] Timestamps format correctly (relative time) + - [ ] Empty state shows when no notifications + - [ ] Error state displays on API failure + +3. **NotificationPreferences** + - [ ] Toggles work for all notification types + - [ ] Phone input appears when SMS enabled + - [ ] Save button updates preferences + - [ ] Success message displays after save + - [ ] Error message displays on failure + - [ ] Information card explains webhook notifications + +### Integration Tests + +```typescript +// Example test structure +describe('NotificationBell', () => { + it('displays unread count badge', () => { + // Test implementation + }); + + it('fetches notifications on open', () => { + // Test implementation + }); + + it('polls for new notifications', () => { + // Test implementation + }); +}); +``` + +## Environment Variables + +The dashboard uses the following environment variable: + +- `VITE_API_BASE_URL`: Backend API base URL (default: `http://localhost:3002`) + +Set in `.env` file: +``` +VITE_API_BASE_URL=http://localhost:3002 +``` + +## Storybook + +All components have Storybook stories for isolated development and testing: + +```bash +npm run storybook +``` + +Stories are located in `src/stories/`: +- `NotificationBell.stories.tsx` +- `NotificationCenter.stories.tsx` +- `NotificationPreferences.stories.tsx` + +## Future Enhancements + +1. **Real-time Updates** + - WebSocket integration for instant notifications + - Server-sent events (SSE) as alternative + +2. **Advanced Filtering** + - Date range filters + - Transaction type filters + - Search functionality + +3. **Notification Actions** + - Mark as read/unread + - Delete notifications + - Archive old notifications + +4. **Rich Notifications** + - Action buttons (e.g., "View Transaction") + - Inline transaction details + - Notification grouping + +5. **Mobile Optimization** + - Responsive design improvements + - Touch-friendly interactions + - Mobile-specific layouts + +## Troubleshooting + +### Notifications not loading + +1. Check authentication token in localStorage +2. Verify API endpoint is accessible +3. Check browser console for errors +4. Verify CORS configuration on backend + +### Preferences not saving + +1. Ensure valid phone number format for SMS +2. Check network tab for API request/response +3. Verify authentication token is valid +4. Check backend logs for errors + +### Polling not working + +1. Check browser console for errors +2. Verify component is mounted +3. Check network tab for periodic requests +4. Ensure no JavaScript errors blocking execution + +## Support + +For issues or questions: +- Check backend logs: `backend/logs/` +- Review API documentation: `backend/docs/` +- Contact: support@anchorpoint.local diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 4df70c6..fe92b3f 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -10,17 +10,21 @@ import { X, Wallet, AlertCircle, + Bell, } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import type { UiConfig } from './types'; import { LogoMark } from './components/LogoMark'; import { RequirementList } from './components/RequirementList'; +import { NotificationBell } from './components/NotificationBell'; // Lazy-load heavy tab views so they are only fetched when first visited const DashboardOverview = lazy(() => import('./components/DashboardOverview')); const TransactionHistory = lazy(() => import('./components/TransactionHistory')); const SEP24Flow = lazy(() => import('./components/SEP24Flow')); const KycStatusView = lazy(() => import('./components/KycStatusView')); +const NotificationCenter = lazy(() => import('./components/NotificationCenter')); +const NotificationPreferences = lazy(() => import('./components/NotificationPreferences')); const defaultUiConfig: UiConfig = { brandName: 'AnchorPoint', @@ -106,6 +110,7 @@ const App = () => { { id: 'deposit', icon: ArrowDownLeft, label: 'Deposit' }, { id: 'withdraw', icon: ArrowUpRight, label: 'Withdraw' }, { id: 'history', icon: History, label: 'History' }, + { id: 'notifications', icon: Bell, label: 'Notifications' }, { id: 'kyc', icon: ShieldCheck, label: 'KYC Status' }, { id: 'settings', icon: Settings, label: 'Settings' }, ], @@ -212,6 +217,10 @@ const App = () => { {loadingState === 'error' ? 'Fallback Theme Active' : 'Config Connected'} + setActiveTab('notifications')} + /> + + + {isOpen && ( + +
+

Notifications

+ +
+ +
+ {loading && notifications.length === 0 ? ( +
+
+
+ ) : error ? ( +
{error}
+ ) : notifications.length === 0 ? ( +
+ +

No notifications yet

+
+ ) : ( +
+ {notifications.slice(0, 5).map((notification) => ( +
+
+
{getStatusIcon(notification.status)}
+
+

{notification.message}

+
+ {notification.type.toLowerCase()} + + {formatTimestamp(notification.createdAt)} +
+
+
+
+ ))} +
+ )} +
+ + {notifications.length > 0 && ( +
+ +
+ )} + + )} + +
+ ); +}; diff --git a/dashboard/src/components/NotificationCenter.tsx b/dashboard/src/components/NotificationCenter.tsx new file mode 100644 index 0000000..7f4e078 --- /dev/null +++ b/dashboard/src/components/NotificationCenter.tsx @@ -0,0 +1,288 @@ +import React, { useState, useEffect } from 'react'; +import { Bell, Check, Clock, AlertCircle, Filter, RefreshCw, Settings } from 'lucide-react'; +import { motion } from 'framer-motion'; + +export interface Notification { + id: string; + userId: string; + transactionId: string | null; + type: 'EMAIL' | 'SMS' | 'PUSH'; + status: 'PENDING' | 'SENT' | 'FAILED'; + message: string; + createdAt: string; +} + +interface NotificationCenterProps { + apiBaseUrl?: string; + onOpenPreferences?: () => void; +} + +export const NotificationCenter: React.FC = ({ + apiBaseUrl = 'http://localhost:3002', + onOpenPreferences, +}) => { + const [notifications, setNotifications] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filter, setFilter] = useState<'all' | 'PENDING' | 'SENT' | 'FAILED'>('all'); + const [refreshing, setRefreshing] = useState(false); + + useEffect(() => { + fetchNotifications(); + }, []); + + const fetchNotifications = async (showRefreshing = false) => { + if (showRefreshing) { + setRefreshing(true); + } else { + setLoading(true); + } + setError(null); + + try { + const token = localStorage.getItem('authToken'); + const response = await fetch(`${apiBaseUrl}/api/notifications/history`, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch notifications'); + } + + const data = await response.json(); + setNotifications(data.data || []); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + console.error('Error fetching notifications:', err); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case 'SENT': + return ; + case 'FAILED': + return ; + case 'PENDING': + return ; + default: + return null; + } + }; + + const getStatusBadge = (status: string) => { + const styles = { + SENT: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20', + FAILED: 'bg-red-500/10 text-red-400 border-red-500/20', + PENDING: 'bg-amber-500/10 text-amber-400 border-amber-500/20', + }; + + return ( + + {getStatusIcon(status)} + {status} + + ); + }; + + const formatTimestamp = (timestamp: string) => { + const date = new Date(timestamp); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins} minutes ago`; + if (diffHours < 24) return `${diffHours} hours ago`; + if (diffDays < 7) return `${diffDays} days ago`; + return date.toLocaleString(); + }; + + const filteredNotifications = + filter === 'all' + ? notifications + : notifications.filter((n) => n.status === filter); + + const stats = { + total: notifications.length, + sent: notifications.filter((n) => n.status === 'SENT').length, + pending: notifications.filter((n) => n.status === 'PENDING').length, + failed: notifications.filter((n) => n.status === 'FAILED').length, + }; + + return ( +
+ {/* Stats Cards */} +
+
+
+
+

Total

+

{stats.total}

+
+ +
+
+ +
+
+
+

Sent

+

{stats.sent}

+
+ +
+
+ +
+
+
+

Pending

+

{stats.pending}

+
+ +
+
+ +
+
+
+

Failed

+

{stats.failed}

+
+ +
+
+
+ + {/* Controls */} +
+
+
+ + Filter: +
+ {(['all', 'PENDING', 'SENT', 'FAILED'] as const).map((f) => ( + + ))} +
+
+ +
+ + + {onOpenPreferences && ( + + )} +
+
+
+ + {/* Notifications List */} +
+ {loading ? ( +
+
+
+ ) : error ? ( +
+ +

{error}

+ +
+ ) : filteredNotifications.length === 0 ? ( +
+ +

+ {filter === 'all' ? 'No notifications yet' : `No ${filter.toLowerCase()} notifications`} +

+

+ Webhook events and transaction updates will appear here +

+
+ ) : ( +
+ {filteredNotifications.map((notification, index) => ( + +
+
{getStatusIcon(notification.status)}
+
+
+

{notification.message}

+ {getStatusBadge(notification.status)} +
+
+ + Type: + {notification.type.toLowerCase()} + + {notification.transactionId && ( + <> + + + Transaction: + {notification.transactionId.slice(0, 8)}... + + + )} + + {formatTimestamp(notification.createdAt)} +
+
+
+
+ ))} +
+ )} +
+
+ ); +}; + +export default NotificationCenter; diff --git a/dashboard/src/components/NotificationPreferences.tsx b/dashboard/src/components/NotificationPreferences.tsx new file mode 100644 index 0000000..928413d --- /dev/null +++ b/dashboard/src/components/NotificationPreferences.tsx @@ -0,0 +1,277 @@ +import React, { useState, useEffect } from 'react'; +import { Mail, MessageSquare, Bell, Save, AlertCircle, CheckCircle } from 'lucide-react'; + +interface NotificationPreferencesProps { + apiBaseUrl?: string; +} + +interface Preferences { + emailEnabled: boolean; + smsEnabled: boolean; + pushEnabled: boolean; + phone?: string; +} + +export const NotificationPreferences: React.FC = ({ + apiBaseUrl = 'http://localhost:3002', +}) => { + const [preferences, setPreferences] = useState({ + emailEnabled: true, + smsEnabled: false, + pushEnabled: false, + phone: '', + }); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + useEffect(() => { + fetchPreferences(); + }, []); + + const fetchPreferences = async () => { + setLoading(true); + setError(null); + + try { + const token = localStorage.getItem('authToken'); + const response = await fetch(`${apiBaseUrl}/api/notifications/preferences`, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch preferences'); + } + + const data = await response.json(); + setPreferences(data.data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + console.error('Error fetching preferences:', err); + } finally { + setLoading(false); + } + }; + + const savePreferences = async () => { + setSaving(true); + setError(null); + setSuccess(false); + + try { + const token = localStorage.getItem('authToken'); + const response = await fetch(`${apiBaseUrl}/api/notifications/preferences`, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(preferences), + }); + + if (!response.ok) { + throw new Error('Failed to save preferences'); + } + + setSuccess(true); + setTimeout(() => setSuccess(false), 3000); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + console.error('Error saving preferences:', err); + } finally { + setSaving(false); + } + }; + + const handleToggle = (key: keyof Preferences) => { + setPreferences((prev) => ({ + ...prev, + [key]: !prev[key], + })); + }; + + const handlePhoneChange = (phone: string) => { + setPreferences((prev) => ({ + ...prev, + phone, + })); + }; + + if (loading) { + return ( +
+
+
+
+
+ ); + } + + return ( +
+
+

Notification Preferences

+ +
+ {/* Email Notifications */} +
+
+
+ +
+
+

Email Notifications

+

+ Receive transaction updates and webhook events via email +

+
+
+ +
+ + {/* SMS Notifications */} +
+
+
+ +
+
+

SMS Notifications

+

+ Receive critical alerts via text message +

+ {preferences.smsEnabled && ( +
+ + handlePhoneChange(e.target.value)} + placeholder="+1 (555) 123-4567" + className="input-field w-full max-w-xs" + /> +
+ )} +
+
+ +
+ + {/* Push Notifications */} +
+
+
+ +
+
+

Push Notifications

+

+ Receive real-time notifications in your browser +

+
+
+ +
+
+ + {/* Save Button */} +
+
+ {error && ( +
+ + {error} +
+ )} + {success && ( +
+ + Preferences saved successfully +
+ )} +
+ +
+
+ + {/* Information Card */} +
+

About Webhook Notifications

+
+

+ Webhook notifications keep you informed about important events in your account: +

+
    +
  • Transaction status changes (pending, completed, failed)
  • +
  • Deposit and withdrawal confirmations
  • +
  • KYC verification updates
  • +
  • Multisig transaction approvals
  • +
  • Security alerts and account activity
  • +
+

+ You can customize which channels receive notifications based on your preferences. + Email notifications are recommended for important updates. +

+
+
+
+ ); +}; + +export default NotificationPreferences; diff --git a/dashboard/src/stories/NotificationBell.stories.tsx b/dashboard/src/stories/NotificationBell.stories.tsx new file mode 100644 index 0000000..297107a --- /dev/null +++ b/dashboard/src/stories/NotificationBell.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { NotificationBell } from '../components/NotificationBell'; + +const meta = { + title: 'Components/NotificationBell', + component: NotificationBell, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + apiBaseUrl: 'http://localhost:3002', + }, +}; + +export const WithCallback: Story = { + args: { + apiBaseUrl: 'http://localhost:3002', + onViewAll: () => alert('View all notifications clicked'), + }, +}; diff --git a/dashboard/src/stories/NotificationCenter.stories.tsx b/dashboard/src/stories/NotificationCenter.stories.tsx new file mode 100644 index 0000000..2a19192 --- /dev/null +++ b/dashboard/src/stories/NotificationCenter.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import NotificationCenter from '../components/NotificationCenter'; + +const meta = { + title: 'Components/NotificationCenter', + component: NotificationCenter, + parameters: { + layout: 'fullscreen', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + apiBaseUrl: 'http://localhost:3002', + }, +}; + +export const WithPreferencesCallback: Story = { + args: { + apiBaseUrl: 'http://localhost:3002', + onOpenPreferences: () => alert('Open preferences clicked'), + }, +}; diff --git a/dashboard/src/stories/NotificationPreferences.stories.tsx b/dashboard/src/stories/NotificationPreferences.stories.tsx new file mode 100644 index 0000000..a30fb18 --- /dev/null +++ b/dashboard/src/stories/NotificationPreferences.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import NotificationPreferences from '../components/NotificationPreferences'; + +const meta = { + title: 'Components/NotificationPreferences', + component: NotificationPreferences, + parameters: { + layout: 'fullscreen', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + apiBaseUrl: 'http://localhost:3002', + }, +};