# 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(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(key: String) -> T?` - `setCached(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(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.