From c76d3367f16c8a7aa9d084d497a843fc26977930 Mon Sep 17 00:00:00 2001
From: milah-247
Date: Sat, 30 May 2026 23:28:35 +0100
Subject: [PATCH] feat: implement webhook event notifications UI
Closes #347
- Add NotificationBell component with unread count badge and dropdown preview
- Add NotificationCenter component with filtering, stats, and full notification list
- Add NotificationPreferences component for managing email/SMS/push preferences
- Integrate notification components into main App with new Notifications tab
- Add Storybook stories for all notification components
- Add comprehensive documentation in WEBHOOK_NOTIFICATIONS.md
Features:
- Real-time notification polling (30s interval)
- Filter notifications by status (All, PENDING, SENT, FAILED)
- Statistics dashboard showing total, sent, pending, and failed counts
- Notification preferences with toggle switches for each channel
- Responsive design with dark theme matching AnchorPoint branding
- Accessibility compliant with ARIA labels and keyboard navigation
- API integration with /api/notifications/history and /api/notifications/preferences
Technical Details:
- TypeScript with full type safety
- Framer Motion animations for smooth transitions
- Tailwind CSS for styling
- Lucide React icons
- Error handling and loading states
- Click-outside detection for dropdown
- Relative timestamp formatting
---
dashboard/WEBHOOK_NOTIFICATIONS.md | 314 ++++++++++++++++++
dashboard/src/App.tsx | 20 ++
dashboard/src/components/NotificationBell.tsx | 210 ++++++++++++
.../src/components/NotificationCenter.tsx | 288 ++++++++++++++++
.../components/NotificationPreferences.tsx | 277 +++++++++++++++
.../src/stories/NotificationBell.stories.tsx | 27 ++
.../stories/NotificationCenter.stories.tsx | 27 ++
.../NotificationPreferences.stories.tsx | 20 ++
8 files changed, 1183 insertions(+)
create mode 100644 dashboard/WEBHOOK_NOTIFICATIONS.md
create mode 100644 dashboard/src/components/NotificationBell.tsx
create mode 100644 dashboard/src/components/NotificationCenter.tsx
create mode 100644 dashboard/src/components/NotificationPreferences.tsx
create mode 100644 dashboard/src/stories/NotificationBell.stories.tsx
create mode 100644 dashboard/src/stories/NotificationCenter.stories.tsx
create mode 100644 dashboard/src/stories/NotificationPreferences.stories.tsx
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')}
+ />
@@ -262,6 +273,15 @@ const App = () => {
{activeTab === 'deposit' && }
{activeTab === 'withdraw' && }
{activeTab === 'history' && }
+ {activeTab === 'notifications' && (
+ setActiveTab('notification-preferences')}
+ />
+ )}
+ {activeTab === 'notification-preferences' && (
+
+ )}
{activeTab === 'kyc' && }
{activeTab === 'settings' && (
diff --git a/dashboard/src/components/NotificationBell.tsx b/dashboard/src/components/NotificationBell.tsx
new file mode 100644
index 0000000..9378e2b
--- /dev/null
+++ b/dashboard/src/components/NotificationBell.tsx
@@ -0,0 +1,210 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Bell, X, Check, Clock, AlertCircle } from 'lucide-react';
+import { motion, AnimatePresence } 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 NotificationBellProps {
+ apiBaseUrl?: string;
+ onViewAll?: () => void;
+}
+
+export const NotificationBell: React.FC
= ({
+ apiBaseUrl = 'http://localhost:3002',
+ onViewAll,
+}) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [notifications, setNotifications] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const dropdownRef = useRef(null);
+
+ const unreadCount = notifications.filter((n) => n.status === 'PENDING').length;
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ };
+
+ if (isOpen) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [isOpen]);
+
+ useEffect(() => {
+ if (isOpen) {
+ fetchNotifications();
+ }
+ }, [isOpen]);
+
+ // Poll for new notifications every 30 seconds
+ useEffect(() => {
+ const interval = setInterval(() => {
+ fetchNotifications();
+ }, 30000);
+
+ return () => clearInterval(interval);
+ }, []);
+
+ const fetchNotifications = async () => {
+ 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);
+ }
+ };
+
+ const getStatusIcon = (status: string) => {
+ switch (status) {
+ case 'SENT':
+ return ;
+ case 'FAILED':
+ return ;
+ case 'PENDING':
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ 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}m ago`;
+ if (diffHours < 24) return `${diffHours}h ago`;
+ if (diffDays < 7) return `${diffDays}d ago`;
+ return date.toLocaleDateString();
+ };
+
+ return (
+
+
+
+
+ {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}
+
+
+
+
+
+
+
+
+
+
+
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 && (
+
+ )}
+ {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',
+ },
+};