> ## Documentation Index
> Fetch the complete documentation index at: https://285e39fd5e337e58f16290.sightscreen.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Services

> The service layer — protocols, real/fake implementations, DI via @Environment, and the DataSource + ViewSubscriber pattern.

## Overview

Every service in the iOS app follows the same pattern:

1. A **Protocol** defines the interface
2. A **Real** implementation (e.g., `CognitoAuthService`) talks to the backend
3. A **Fake** implementation returns canned data for previews and tests
4. All services are `@Observable` and injected via `@Environment`

```swift theme={null}
// The pattern — repeated for every service
protocol ScheduleServiceProtocol {
    func fetchSchedule(for date: Date) async throws -> ScheduleResponse
}

@Observable
class RealScheduleService: ScheduleServiceProtocol { /* hits API */ }

@Observable
class FakeScheduleService: ScheduleServiceProtocol { /* returns mock data */ }

// Injection
@Environment(\.scheduleService) private var scheduleService
```

***

## AppBootstrap

**Responsibility:** Cold-start phase gating.

AppBootstrap orchestrates the app launch sequence, ensuring services initialize in the correct order before the UI becomes interactive.

| Phase           | What happens                                                              |
| --------------- | ------------------------------------------------------------------------- |
| 1. Config check | `AppConfigService` checks version + maintenance status                    |
| 2. Auth restore | `AuthService` restores the session from the Keychain                      |
| 3. Follow sync  | `FollowManager` calls `GET /user/follows` to reconcile local state        |
| 4. LA restore   | `LifecycleManager` re-attaches observers to any surviving Live Activities |
| 5. Ready        | UI unblocks, data sources begin polling                                   |

<Note>
  If any phase fails (e.g., maintenance mode detected), AppBootstrap halts and shows the appropriate blocking screen. The user never sees a partially initialized app.
</Note>

***

## AuthService

**Protocol:** `AuthService`
**Real implementation:** `CognitoAuthService`

Handles all Cognito-based authentication flows.

| Method                                    | Purpose                                   |
| ----------------------------------------- | ----------------------------------------- |
| `signUp(email:password:)`                 | Create a new Cognito user                 |
| `signIn(email:password:)`                 | Authenticate and receive tokens           |
| `verifyEmail(code:)`                      | Confirm email with verification code      |
| `resetPassword(email:)`                   | Initiate forgot-password flow             |
| `confirmResetPassword(code:newPassword:)` | Complete password reset                   |
| `signOut()`                               | Clear tokens and session                  |
| `currentUser`                             | The currently authenticated user (if any) |

### Admin detection

Admin status is determined by checking the JWT token's `cognito:groups` claim. If the user belongs to the `admin` group, `AppUser.isAdmin` is `true`.

<Note>
  Admin detection is read-only on the client. The backend enforces admin permissions independently via its own JWT validation.
</Note>

***

## LiveActivityService (facade)

**Protocol:** `LiveActivityService`
**Real implementation:** `RealLiveActivityService`

The facade composes three managers and exposes a unified API to the rest of the app. Views and other services interact with `LiveActivityService` — never with the managers directly.

```mermaid theme={null}
flowchart LR
    View -->|follow / unfollow| LAS[LiveActivityService]
    LAS --> TM[TokenManager]
    LAS --> LM[LifecycleManager]
    LAS --> FM[FollowManager]
```

| Delegated to                   | Responsibilities                                                                                                              |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `LiveActivityTokenManager`     | Push-to-start token observation, per-activity update token observation, POST tokens to backend, deduplication                 |
| `LiveActivityLifecycleManager` | `Activity.request()`, adopt push-started LAs, dismiss observation, restore on relaunch, stale check, content observation      |
| `FollowManager`                | Follow/unfollow API, `followedMatchIds` persistence, retry pending unfollows, bell icon states, sync from `GET /user/follows` |

<Warning>
  The LiveActivityService facade is the most stateful part of the app. See the [Live Activities](/ios/live-activities) page for the full lifecycle, token management, and push-to-start flow.
</Warning>

***

## NotificationService

**Protocol:** `NotificationService`
**Real implementation:** `RealNotificationService`

Handles push notification permissions and device registration with dual token management.

| Method                   | Purpose                                                 |
| ------------------------ | ------------------------------------------------------- |
| `requestPermission()`    | Prompts the user for notification permission            |
| `registerDevice(token:)` | Sends the APNs device token to the backend              |
| `unregisterDevice()`     | Tells the backend to stop sending pushes to this device |

### Dual tokens

The app manages two distinct APNs tokens:

| Token               | Purpose                                                                                             |
| ------------------- | --------------------------------------------------------------------------------------------------- |
| Device token        | Standard push notifications (alerts, badges, sounds)                                                |
| Push-to-start token | Starting Live Activities remotely (managed by `TokenManager`, registered via `NotificationService`) |

Both tokens are registered with the backend on launch and whenever they rotate.

***

## ScheduleService

**Protocol:** `ScheduleService`
**Real implementation:** `RealScheduleService`

Fetches and caches match schedules.

| Feature              | Detail                                                                |
| -------------------- | --------------------------------------------------------------------- |
| Sliding-window cache | Keeps N days of schedule data in memory to avoid redundant fetches    |
| Debouncing           | Rapid date changes debounce API calls to avoid flooding the backend   |
| Today refresh        | Always re-fetches today's schedule to ensure live matches are current |

***

## DataSource protocol + ViewSubscriber modifier

A generic pattern for data that needs to be fetched, cached, and optionally polled.

### DataSource protocol

```swift theme={null}
protocol DataSource<T> {
    associatedtype T

    var data: T? { get }

    func load() async throws -> T
    func refresh() async throws -> T
    func shouldPoll() -> Bool
    var pollIntervalMs: Int { get }
}
```

| Method / Property | Purpose                                                               |
| ----------------- | --------------------------------------------------------------------- |
| `load()`          | Cache-first read. Returns cached data if available, otherwise fetches |
| `refresh()`       | Forces a fresh fetch, bypassing cache                                 |
| `shouldPoll()`    | Returns whether this data source needs active polling right now       |
| `pollIntervalMs`  | Polling interval in milliseconds (can be server-driven)               |

### ViewSubscriber modifier

A SwiftUI view modifier that binds a `DataSource` to a view's lifecycle.

```swift theme={null}
SomeView()
    .viewSubscriber(dataSource: liveMatchesDataSource)
```

| Behavior             | Detail                                                                      |
| -------------------- | --------------------------------------------------------------------------- |
| Lifecycle binding    | Starts polling on `onAppear`, stops on `onDisappear`                        |
| ScenePhase awareness | Pauses polling when app enters background, resumes on foreground            |
| Cache-first          | On appear, immediately shows cached data while refreshing in the background |

<Tip>
  `ViewSubscriber` eliminates the need for each view to manually manage polling timers and lifecycle events. Attach it once and the data source handles the rest.
</Tip>

### LiveMatchesDataSource

| Feature                | Detail                                                                                         |
| ---------------------- | ---------------------------------------------------------------------------------------------- |
| Always polls           | `shouldPoll()` always returns `true` while there are live matches                              |
| Server-driven interval | `pollIntervalMs` comes from the backend's `retryAfterMs` field in the response                 |
| Adapts dynamically     | If the server says "poll again in 5s" during a key moment, the interval tightens automatically |

### ScheduleDataSource

| Feature                         | Detail                                                           |
| ------------------------------- | ---------------------------------------------------------------- |
| Per-date                        | Each date has its own cache entry                                |
| Conditional polling             | Only polls if the selected date is today (past dates are static) |
| Integrates with ScheduleService | Uses the sliding-window cache underneath                         |

***

## MatchCacheStore

Three-layer cache for match data with intelligent invalidation.

### Cache layers

| Layer        | Component                                    | Access time | Detail                                                                                                                                                             |
| ------------ | -------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 1. In-memory | `MatchCacheStore`                            | Microsecond | Date-indexed dictionary for O(1) schedule lookups. Fast reads for active views.                                                                                    |
| 2. Disk      | `DiskCacheStore` via `MatchDiskCacheWrapper` | \~5ms       | Survives app restarts. Uses schema-versioned invalidation — when `MATCH_SCHEMA_VERSION` bumps, the entire disk cache is discarded to avoid decoding stale formats. |
| 3. HTTP      | `URLSession` to `/matches`                   | 500ms+      | Network fetch from the backend API. Only hit when layers 1 and 2 miss.                                                                                             |

### Hydration on launch

`loadFromDisk()` is called during `AppBootstrap` to hydrate the in-memory cache from disk before other boot tasks run. This means the app can render cached schedules immediately, even before the first network response arrives.

### TTL by state

| Match state | TTL behavior                                                               |
| ----------- | -------------------------------------------------------------------------- |
| Live        | Short TTL — always refreshed from the network to ensure real-time accuracy |
| Completed   | Long TTL — stable data, served from cache without re-fetching              |

<Warning>
  **Swift 6 gotcha:** Synthesized `Codable` conformance on structs is main-actor-isolated in Swift 6. The cache model structs require explicit `nonisolated init(from:)` and `nonisolated encode(to:)` implementations to allow background encoding/decoding by `DiskCacheStore`. Without this, the compiler will emit a concurrency error when disk I/O runs off the main actor.
</Warning>

***

## ImageCacheService

**Protocol:** `ImageCacheService`
**Real implementation:** `RealImageCacheService`

Two-tier image cache for team logos and assets.

| Tier           | Detail                                                               |
| -------------- | -------------------------------------------------------------------- |
| Memory cache   | `NSCache`-based, evicted under memory pressure                       |
| Disk cache     | File-based in the **app group shared container**                     |
| Async prefetch | Prefetches images for upcoming matches so they are ready when needed |

<Note>
  The disk cache uses the app group container so both the main app and the widget target can access cached images. This avoids duplicate downloads for team logos shown in Live Activities.
</Note>

***

## AppConfigService

**Protocol:** `AppConfigService`
**Real implementation:** `RealAppConfigService`

Checks app configuration on launch.

| Feature          | Detail                                                                    |
| ---------------- | ------------------------------------------------------------------------- |
| Version check    | Compares local app version against minimum required version from backend  |
| Force update     | If the app is below minimum version, blocks usage and shows update prompt |
| Maintenance mode | If backend reports maintenance, shows a maintenance screen                |

<Tip>
  The version check runs on every app launch. The backend controls the minimum version, so you can force-update users without an App Store review cycle.
</Tip>

***

## AdminService

**Protocol:** `AdminService`
**Real implementation:** `RealAdminService`

Debug-only service for controlling fake mode.

| Feature           | Detail                                                        |
| ----------------- | ------------------------------------------------------------- |
| Fake API controls | Toggle fake API mode, switch between canned responses         |
| Tape playback     | Replay recorded API responses for testing Live Activity flows |

<Note>
  This service is only available in debug builds. It connects to `fake-api.sightscreen.app` for testing.
</Note>

***

## AppLogger

Unified logging across three destinations.

| Destination       | Purpose                                                            |
| ----------------- | ------------------------------------------------------------------ |
| `os.Logger`       | System-level structured logging (visible in Console.app and Xcode) |
| Sentry            | Error and breadcrumb reporting for production crash analysis       |
| `DebugLogService` | In-app debug log overlay for on-device troubleshooting             |

```swift theme={null}
AppLogger.info("Token registered", category: .liveActivity)
AppLogger.error("Failed to adopt push-started LA", error: error, category: .liveActivity)
```

<Tip>
  Use `AppLogger` for all logging. Never use `print()` or raw `os_log` calls. `AppLogger` ensures every log event reaches all three destinations with consistent formatting.
</Tip>

***

## DI wiring

All services are wired at the app entry point and injected into the SwiftUI environment:

```swift theme={null}
@main
struct SightscreenApp: App {
    var body: some Scene {
        WindowGroup {
            RootView()
                .environment(\.authService, RealCognitoAuthService())
                .environment(\.scheduleService, RealScheduleService())
                .environment(\.liveActivityService, RealLiveActivityService())
                .environment(\.notificationService, RealNotificationService())
                .environment(\.appConfigService, RealAppConfigService())
                .environment(\.imageCacheService, RealImageCacheService())
                .environment(\.matchCacheStore, RealMatchCacheStore())
        }
    }
}
```

For SwiftUI previews, swap in fakes:

```swift theme={null}
#Preview {
    MatchListView()
        .environment(\.scheduleService, FakeScheduleService())
        .environment(\.liveActivityService, FakeLiveActivityService())
}
```
