rebranding
This commit is contained in:
128
tasks/kordant-unified-restructure/31-ios-api-client.md
Normal file
128
tasks/kordant-unified-restructure/31-ios-api-client.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# 31. iOS App — API Client, tRPC Bridge, and Offline Support
|
||||
|
||||
meta:
|
||||
id: kordant-unified-restructure-31
|
||||
feature: kordant-unified-restructure
|
||||
priority: P1
|
||||
depends_on: [kordant-unified-restructure-28, kordant-unified-restructure-29, kordant-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/Kordant/Services/APIClient.swift` — HTTP client:
|
||||
- Wraps `URLSession` with async/await (`async throws` methods)
|
||||
- Base URL from environment/config (`https://api.kordant.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/Kordant/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/Kordant/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/Kordant/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/Kordant/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/Kordant/Services/NetworkMonitor.swift` — Connectivity monitoring:
|
||||
- Uses `NWPathMonitor` to track network state
|
||||
- Publishes `isConnected` boolean
|
||||
- Triggers queue processing when connection restored
|
||||
|
||||
steps:
|
||||
1. Create `iOS/Kordant/Services/` and `iOS/Kordant/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 `kordant.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.
|
||||
Reference in New Issue
Block a user