Files
Kordant/tasks/shieldai-unified-restructure/31-ios-api-client.md
2026-05-25 12:23:23 -04:00

129 lines
7.0 KiB
Markdown

# 31. iOS App — API Client, tRPC Bridge, and Offline Support
meta:
id: shieldai-unified-restructure-31
feature: shieldai-unified-restructure
priority: P1
depends_on: [shieldai-unified-restructure-28, shieldai-unified-restructure-29, shieldai-unified-restructure-30]
tags: [ios, swift, api, networking, offline, mobile]
objective:
- Build the API client layer for the iOS app that communicates with the unified monolith's tRPC endpoints. Since tRPC is TypeScript-native, we'll use a thin HTTP bridge approach: the iOS client calls tRPC procedures as typed HTTP JSON requests. Add offline support with local caching and request queuing.
deliverables:
- `iOS/ShieldAI/Services/APIClient.swift` — HTTP client:
- Wraps `URLSession` with async/await (`async throws` methods)
- Base URL from environment/config (`https://api.shieldai.com` or local)
- Automatic auth header injection (JWT from Keychain)
- Request/response logging in debug builds
- Retry logic with exponential backoff for network errors
- Timeout configuration
- `iOS/ShieldAI/Services/TRPCBridge.swift` — tRPC procedure caller:
- `callProcedure<T: Decodable>(path: String, input: Encodable?) async throws -> T`
- Serializes input to JSON, sends POST to `/api/trpc/{path}`
- Deserializes response JSON to Swift `Decodable` type
- Handles tRPC error format (`{ error: { message, code } }`)
- Type-safe wrappers for common procedures:
- `user.me() -> User`
- `billing.getSubscription() -> Subscription`
- `darkwatch.getWatchlist() -> [WatchlistItem]`
- etc.
- `iOS/ShieldAI/Models/` — Swift data models:
- `User.swift`, `Subscription.swift`, `WatchlistItem.swift`, `Exposure.swift`, `Alert.swift`, `VoiceEnrollment.swift`, `VoiceAnalysis.swift`, `SpamRule.swift`, `PropertyWatchlistItem.swift`, `RemovalRequest.swift`, `BrokerListing.swift`, `NormalizedAlert.swift`, `CorrelationGroup.swift`, `SecurityReport.swift`
- All models conform to `Codable`, `Identifiable`, `Equatable`
- Enum types for Swift (e.g., `SubscriptionTier`, `AlertSeverity`, `ExposureSource`)
- `iOS/ShieldAI/Services/CacheManager.swift` — Offline cache:
- `CacheEntry` with data, timestamp, TTL
- Stores in `UserDefaults` (small data) or file system (large data)
- `getCached<T>(key: String) -> T?`
- `setCached<T>(key: String, value: T, ttl: TimeInterval)`
- Automatic cache invalidation on TTL expiry
- `iOS/ShieldAI/Services/OfflineQueue.swift` — Request queue:
- Queues mutations when offline
- Retries queued requests when connectivity restored
- Persists queue to disk
- `addToQueue(request: QueuedRequest)`
- `processQueue()` — called when network becomes available
- `iOS/ShieldAI/Services/NetworkMonitor.swift` — Connectivity monitoring:
- Uses `NWPathMonitor` to track network state
- Publishes `isConnected` boolean
- Triggers queue processing when connection restored
steps:
1. Create `iOS/ShieldAI/Services/` and `iOS/ShieldAI/Models/` directories.
2. **APIClient**:
- Create `APIClient` class as `@Observable` or singleton
- `request<T: Decodable>(endpoint: String, method: String, body: Data?) async throws -> T`
- Add request interceptor for auth header
- Add response interceptor for error parsing
- Use `JSONEncoder`/`JSONDecoder` with custom date formatting
3. **TRPCBridge**:
- `callProcedure` constructs tRPC batch request format:
```json
{ "0": { "json": { "email": "test@example.com" } } }
```
- Parse tRPC response format:
```json
{ "0": { "result": { "data": { ... } } } }
```
- Map tRPC error codes to Swift `APIError` enum
- Create convenience methods for each router procedure
4. **Models**:
- Define Swift structs matching Drizzle schema shapes
- Use `CodingKeys` where JSON keys differ from Swift naming
- Define enums for all database enums (e.g., `SubscriptionTier: String, Codable`)
- Add computed properties where needed (e.g., `Alert.isCritical: Bool`)
5. **CacheManager**:
- Use `JSONEncoder` to serialize values
- Store in `UserDefaults` with key prefix `shieldai.cache.`
- TTL check: compare `Date()` to stored timestamp
- Clear all cache on logout
6. **OfflineQueue**:
- `QueuedRequest` struct: `endpoint`, `method`, `body`, `timestamp`, `retryCount`
- Store array in `UserDefaults` or JSON file
- `processQueue`: iterate requests, call APIClient, remove on success, increment retry on failure
- Max retries: 3, then mark as failed and notify user
7. **NetworkMonitor**:
- `NWPathMonitor` with `.queue = .main`
- `@Published var isConnected: Bool`
- On change to connected: trigger `OfflineQueue.processQueue()`
8. Create `APIConfig.swift`:
- `baseURL`, `timeout`, `maxRetries` from environment or plist
- Different values for debug/release builds
9. Test all components with mocked URLSession.
steps:
- Unit: APIClient injects auth header correctly
- Unit: TRPCBridge parses tRPC response format correctly
- Unit: CacheManager stores and retrieves values with TTL
- Unit: OfflineQueue persists requests and processes them in order
- Unit: NetworkMonitor publishes correct connectivity state
- Integration: APIClient successfully calls `user.me` against local dev server
acceptance_criteria:
- [ ] APIClient makes authenticated HTTP requests to tRPC endpoints
- [ ] TRPCBridge correctly serializes tRPC batch input and deserializes responses
- [ ] All common API procedures have type-safe Swift wrappers
- [ ] Network errors trigger retry with exponential backoff
- [ ] CacheManager stores GET responses and returns cached data when offline
- [ ] OfflineQueue persists mutations and retries when connectivity restored
- [ ] NetworkMonitor accurately tracks connectivity state
- [ ] All models are Codable and match backend schema shapes
- [ ] API configuration supports different environments (dev, staging, prod)
validation:
- Point APIClient to local dev server (`http://localhost:3000`)
- Call `user.me()` and verify response parsed into `User` model
- Disconnect network, attempt a mutation, verify it queues
- Reconnect network, verify queued mutation executes automatically
- Verify cache hit by calling same endpoint twice with network disabled
- Run unit tests via Xcode Cmd+U
notes:
- Since tRPC is TypeScript-native, the iOS client cannot use tRPC's type-safe client directly. The HTTP bridge is the pragmatic approach.
- Consider generating Swift models and API wrappers automatically from the tRPC router types using a code generation tool (e.g., custom script parsing tRPC router exports). For now, manual definitions are fine.
- The tRPC batch link sends multiple procedures in one HTTP request. For simplicity, the iOS client can use single-procedure requests (`/api/trpc/user.me`) instead of batching.
- Use `OSLog` for structured logging in debug builds. Avoid printing sensitive data (tokens, passwords).
- For large responses (e.g., full alert history), consider pagination and storing pages in cache separately.
- The offline queue should only persist safe mutations (idempotent or retry-safe). Avoid queuing destructive operations without user confirmation.