FRE-4738: Implement mark-as-read and mark-all-read actions

- Extract NotificationItem/NotificationType to Models/Notification.swift
- Create NotificationsServiceProtocol with testable service layer
- Implement markAsRead(id:) and markAllAsRead() with HTTP calls
- Add NotificationError enum with localized descriptions
- Update NotificationsViewModel to use protocol-based service
- Add 18 unit tests (12 ViewModel + 6 Model) with mock service
- Update README with architecture documentation
This commit is contained in:
Senior Engineer
2026-05-03 12:17:15 -04:00
committed by Michael Freno
parent 4f1ff9dbb0
commit 57eb01f5af
5 changed files with 583 additions and 136 deletions

View File

@@ -6,59 +6,90 @@ SwiftUI implementation of the notifications feature for the Lendair iOS app.
## Architecture
### MVVM Pattern
- **View**: `NotificationsView` - Main container view
- **ViewModel**: `NotificationsViewModel` - Manages notification state and business logic
- **Service**: `NotificationsService` - Data layer for API communication
- **View**: `Views/` - SwiftUI views for notification display
- **ViewModel**: `ViewModels/` - State management and business logic
- **Service**: `Services/` - Data layer with API communication
- **Model**: `Models/` - Data structures and type definitions
### Components
### File Structure
```
Lendair/
├── Models/
│ └── Notification.swift # NotificationItem, NotificationType, API response types
├── Services/
│ └── NotificationService.swift # NotificationsServiceProtocol + implementation
├── ViewModels/
│ └── NotificationsViewModel.swift # State management, mark-as-read actions
├── Views/
│ ├── NotificationsView.swift # Main notifications list screen
│ └── NotificationRowView.swift # Individual notification row
└── README.md
```
#### NotificationsView (`Views/NotificationsView.swift`)
## Components
### NotificationsView (`Views/NotificationsView.swift`)
- Main navigation container for the notifications screen
- Implements pull-to-refresh functionality
- Handles empty state display
- Provides "Mark All Read" action in toolbar
- Integrates with navigation stack
- Pull-to-refresh via `.refreshable`
- Empty state when no notifications
- "Mark All Read" toolbar button when unread count > 0
- Tap-to-mark-as-read on individual rows
- Swipe-to-delete (TODO: backend integration)
#### NotificationRowView (`Views/NotificationRowView.swift`)
### NotificationRowView (`Views/NotificationRowView.swift`)
- Individual notification list item
- Displays notification icon, title, message, and timestamp
- Shows read/unread indicator
- Supports tap-to-mark-as-read interaction
- Type-specific SF Symbol icon with color coding
- Read/unread indicator (blue dot)
- Relative timestamp display
#### NotificationsViewModel (`ViewModels/NotificationsViewModel.swift`)
- Observable object managing notification state
- Fetches notifications from service layer
- Handles mark-as-read and mark-all-as-read operations
- Calculates unread count for badge display
- Implements refresh logic
### NotificationsViewModel (`ViewModels/NotificationsViewModel.swift`)
- `@Published notifications` — sorted by createdAt descending
- `@Published isLoading` — loading state for UI feedback
- `@Published error` — typed error state (NotificationError)
- `fetchNotifications()` — loads from service
- `markAsRead(id:)` — marks single notification, updates local state
- `markAllAsRead()` — marks all unread, updates local state
- `unreadCount` — computed property for badge display
### NotificationsService (`Services/NotificationService.swift`)
- Protocol: `NotificationsServiceProtocol` (Sendable, testable)
- `list(params:)` — GET `/api/notifications?limit=&offset=`
- `markAsRead(id:)` — PATCH `/api/notifications/:id/read`
- `markAllAsRead()` — PATCH `/api/notifications/read-all`
- Error handling: `NotificationError` enum with localized descriptions
- Configurable: baseURL, URLSession, authToken
### Models (`Models/Notification.swift`)
- `NotificationItem` — Identifiable, Equatable, Codable
- `NotificationType` — 6 cases with icon/color mappings
- `NotificationListParams` — pagination parameters
- `NotificationListResponse`, `NotificationMarkAsReadResponse`, `NotificationMarkAllReadResponse` — API response types
## Notification Types
The app supports the following notification types:
- `LOAN_APPROVED` - Green checkmark icon
- `LOAN_REJECTED` - Red X icon
- `PAYMENT_RECEIVED` - Green down arrow icon
- `PAYMENT_DUE` - Orange exclamation icon
- `NEW_LENDER` - Blue person icon
- `SYSTEM_UPDATE` - Gray info icon
| Type | Icon | Color |
|------|------|-------|
| `LOAN_APPROVED` | checkmark.circle.fill | Green |
| `LOAN_REJECTED` | xmark.circle.fill | Red |
| `PAYMENT_RECEIVED` | arrow.down.circle.fill | Green |
| `PAYMENT_DUE` | exclamationmark.circle.fill | Orange |
| `NEW_LENDER` | person.circle.fill | Blue |
| `SYSTEM_UPDATE` | info.circle.fill | Gray |
## Integration Points
## API Endpoints
### tRPC Router (TODO)
The service layer is designed to connect to the tRPC notifications router:
```typescript
// web/src/server/api/routers/notifications.ts
notifications: router({
list: protectedQuery(...),
markAsRead: protectedMutation(...),
markAllAsRead: protectedMutation(...),
})
```
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/notifications?limit=&offset=` | List notifications |
| PATCH | `/api/notifications/:id/read` | Mark single as read |
| PATCH | `/api/notifications/read-all` | Mark all as read |
### API Endpoints (TODO)
- `GET /api/notifications` - List notifications
- `PATCH /api/notifications/:id/read` - Mark single as read
- `PATCH /api/notifications/read-all` - Mark all as read
## Testing
Tests are in `LendairTests/NotificationServiceTests.swift`:
- 12 ViewModel tests (fetch, mark-as-read, mark-all-read, unread count, refresh, error handling)
- 6 Model tests (icons, colors, equality, raw values, params)
- Uses `MockNotificationsService` conforming to `NotificationsServiceProtocol`
## Usage
@@ -69,15 +100,6 @@ NavigationStack {
}
```
## Testing
Run the preview in Xcode to see the notification row designs:
```swift
#Preview {
NotificationRowView(notification: sampleNotification)
}
```
## Future Enhancements
1. **Push Notifications**: Integrate with UNUserNotificationCenter