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

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 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:
      { "0": { "json": { "email": "test@example.com" } } }
      
    • Parse tRPC response format:
      { "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.