7.0 KiB
7.0 KiB
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
URLSessionwith async/await (async throwsmethods) - Base URL from environment/config (
https://api.shieldai.comor local) - Automatic auth header injection (JWT from Keychain)
- Request/response logging in debug builds
- Retry logic with exponential backoff for network errors
- Timeout configuration
- Wraps
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
Decodabletype - Handles tRPC error format (
{ error: { message, code } }) - Type-safe wrappers for common procedures:
user.me() -> Userbilling.getSubscription() -> Subscriptiondarkwatch.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:CacheEntrywith 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
NWPathMonitorto track network state - Publishes
isConnectedboolean - Triggers queue processing when connection restored
- Uses
steps:
- Create
iOS/ShieldAI/Services/andiOS/ShieldAI/Models/directories. - APIClient:
- Create
APIClientclass as@Observableor 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/JSONDecoderwith custom date formatting
- Create
- TRPCBridge:
callProcedureconstructs tRPC batch request format:{ "0": { "json": { "email": "test@example.com" } } }- Parse tRPC response format:
{ "0": { "result": { "data": { ... } } } } - Map tRPC error codes to Swift
APIErrorenum - Create convenience methods for each router procedure
- Models:
- Define Swift structs matching Drizzle schema shapes
- Use
CodingKeyswhere 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)
- CacheManager:
- Use
JSONEncoderto serialize values - Store in
UserDefaultswith key prefixshieldai.cache. - TTL check: compare
Date()to stored timestamp - Clear all cache on logout
- Use
- OfflineQueue:
QueuedRequeststruct:endpoint,method,body,timestamp,retryCount- Store array in
UserDefaultsor JSON file processQueue: iterate requests, call APIClient, remove on success, increment retry on failure- Max retries: 3, then mark as failed and notify user
- NetworkMonitor:
NWPathMonitorwith.queue = .main@Published var isConnected: Bool- On change to connected: trigger
OfflineQueue.processQueue()
- Create
APIConfig.swift:baseURL,timeout,maxRetriesfrom environment or plist- Different values for debug/release builds
- 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.meagainst 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 intoUsermodel - 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
OSLogfor 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.